Copying a struct; why does this work?

I'm surprised why this compiles and behaves as expected for an AVR based board (Nano). I actually did not expect it to work correctly.

struct TOKEN
{
  float val;
  char op;
};

const uint8_t maxTokens = 20;
struct LIST
{
  // number of tokens currently in list
  uint8_t size;
  // array of tokens
  TOKEN tokens[maxTokens];
};

LIST l1, l2;

void setup()
{
  Serial.begin(115200);

  l1.tokens[0] = { 3, '+' };
  l1.size++;
  l2.tokens[0] = { 4, '/' };
  l2.size++;
  Serial.println(F("l1"));
  Serial.print(F("  "));
  Serial.print(l1.tokens[0].val);
  Serial.print(", ");
  Serial.println(l1.tokens[0].op);

  Serial.println(F("l2"));
  Serial.print(F("  "));
  Serial.print(l2.tokens[0].val);
  Serial.print(", ");
  Serial.println(l2.tokens[0].op);

  l1 = l2;

  Serial.println(F("l1 after 'copy'"));
  Serial.print(F("  "));
  Serial.print(l1.tokens[0].val);
  Serial.print(", ");
  Serial.println(l1.tokens[0].op);
}

void loop()
{
}

This is a stripped down version of a far larger program (that was thrown at me) to show what happens. I would expect the compiler to complain about the line l1 = l2 but it does not.

I would normally use memcpy() to copy structs so the question is if l1 = l2 is correct and why.

The output is

15:32:33.273 -> l1
15:32:33.273 ->   3.00, +
15:32:33.273 -> l2
15:32:33.273 ->   4.00, /
15:32:33.273 -> l1 after 'copy'
15:32:33.305 ->   4.00, /

You just overwrote your l1 pointer with your l2 pointer, so both l1 and l2 point to the content of l2.

1 Like

In which case I don't seem to know what pointers are.

I do understand pointers as in

int i = 3;
int *ptr = &i;

Or in

void someFunc(char *x)
{
}

Print the contents of l1 and l2 before and after the assignment, I think that will bring it home.

The code displays the content before and after copying. But maybe I should print the addresses as well to figure out what happens.

I am 99.99999% sure I did this when I was a noob C programmer a veeeeeerrrrrry long time ago.
Print the address of l1 and l2, print the contents of l1 and l2. Do that before and after the assignment (l1 = l2). What do the prints tell you.
BTW @camsysca was the first to give you the answer.

char buf[64];

struct TOKEN
{
  float val;
  char op;
};

const uint8_t maxTokens = 20;
struct LIST
{
  // number of tokens currently in list
  uint8_t size;
  // array of tokens
  TOKEN tokens[maxTokens];
};

LIST l1, l2;

void setup()
{
  Serial.begin(115200);

  l1.tokens[0] = { 3, '+' };
  l1.size++;
  l2.tokens[0] = { 4, '/' };
  l2.size++;

  sprintf(buf, "address of l1 = %p", &l1);
  Serial.println(buf);
  Serial.println(F("l1"));
  Serial.print(F("  "));
  Serial.print(l1.tokens[0].val);
  Serial.print(", ");
  Serial.println(l1.tokens[0].op);

  sprintf(buf, "address of l2 = %p", &l2);
  Serial.println(buf);
  Serial.println(F("l2"));
  Serial.print(F("  "));
  Serial.print(l2.tokens[0].val);
  Serial.print(", ");
  Serial.println(l2.tokens[0].op);

  l1 = l2;

  sprintf(buf, "address of l1 = %p", &l1);
  Serial.println(buf);
  Serial.println(F("l1 after 'copy'"));
  Serial.print(F("  "));
  Serial.print(l1.tokens[0].val);
  Serial.print(", ");
  Serial.println(l1.tokens[0].op);
}

void loop()
{
}

Output

16:08:31.561 -> address of l1 = 0x1b9
16:08:31.561 -> l1
16:08:31.561 ->   3.00, +
16:08:31.561 -> address of l2 = 0x154
16:08:31.561 -> l2
16:08:31.596 ->   4.00, /
16:08:31.596 -> address of l1 = 0x1b9
16:08:31.596 -> l1 after 'copy'
16:08:31.596 ->   4.00, /

The address of l1 does not change.

Of course it doesn't, but the contents will as it now contains the same value as l2 after statement l1=l2 is executed.

It is impossible I think to change the address of a variable during execution with 'normal' code. It may be doable, but why?
I apologize if I am being too simplistic, nor intent to insult/embarass.

l1 and l2 are 4 (I didn't check but seems more likely than not) bytes of memory that CONTAIN the memory location of a LIST variable.
Copying one LIST variable to another is as simple as l1 = l2; That staement says the 4 bytes of memory we call l1 now contains the 4 bytes of memory we call l2. Only 4 bytes are copied, not the entire list variable.

I think so as well. My point is why @camsysca mentioned pointers; for me the variables are normal variables, not pointers to memory locations. Although they might be under the hood.

As mentioned, I never used an assigment to copy one struct to another, I always used (for the last 35 years or so) memcpy().

Don't worry, that's not that easy :slight_smile: I'm only embarrassed because I don't understand how it works.

I may be off base, C training was 40 years ago, but ISTR that was possible. May, however, be wrong. I've made a mistake, or two, along the way.

Did you enable verbose compilation, and check through the messages? You may be getting warnings. Or not.

I'd be memcpy'ing too..
it's the Default Assignment Operator..
i had to look it up..

~q

I only set "compiler warnings" to "All" and there are no indicators. There is however no difference with verbose on and off.

Do I understand it correctly that the variable l1 is not the actual struct but, under the hood, contains a pointer to the actual struct that is somewhere else in memory; same for l2. That would explain why assignment works.

YES, in every language I ever used but maybe some are different.
Next is array's. Or structs with arrays, or arrays of structs.
If I was to guess, during the time when I got paid to write code, probably 1/3 to 1/2 of the lines of code involved pointers.

Teensy tiny correction, I would not say l1 contains a pointer, l1 IS a pointer to the actual struct.
If the struct is at memory location 1000 and the variable l1 is at 2000 then memory location 2000 (for 4 bytes) contains 1000.
Is that clear?

For me
LIST l1 is a variable containing a LIST.
LIST *l1 is a pointer to some memory that contains a LIST (after malloc/new)

I know that I can generate an assembly listing to see what exactly happens, just don't have the time for it.

1 Like

it's all OK. struct instances behave like variables in some way and copy a struct into another one is fine.

The assignment l1 = l2 performs a shallow, member-wise copy of all the fields in l2 to l1 , which is valid here because no pointers or dynamic allocations are involved.

Since tokens is a fixed-size array inside the struct, it gets copied element by element as part of that member-wise copy ➜ so the array content is safely and entirely copied.

If you use raw char* pointers in the structure pointing somewhere into the heap, then that would be an issue as the shallow copy would copy the pointer and not the data pointed.

5 Likes

Those are the same.

One would hope that if the above were the case the compiler would throw an error of some severity, otherwise a run time error or wose still no error but memory being modified unintentionally will happen.

Thanks people for all the input, appreciated.

Topic solved.