Best practices for making a header/library

PieterP:
A reference is just a pointer with some syntactic sugar on top, it still occupies the same amount of memory as a pointer

Not sure about this..

char d1 = 123;
char d2 = 76;
char& ref = d1;
char* ptr = &d1;

void setup()
{
    //Prevent optimization from removing d1 and d2
    if (d1 == 100) d2++;

    //Print it all
    Serial.println(d1);
    Serial.println(d2);
    Serial.println(ref);
    Serial.println(*ptr);
  
    //Uncommenting the next line will disallow the compiler from
    //optimizing away the pointer causing 2 more bytes to be used 
    //in dynamic memory
    //ptr = &d2;

    //Uncomment next line makes no change to dynamic memory usage
    //ref = d2;
    
    Serial.println(ref);
    Serial.println(*ptr);
}

void loop() {
    
}

The difference you're seeing is because the two lines do something completely different:

ptr = &d2;

This changes the value of the pointer itself, the pointer now points to d2 instead of d1, the value of d1 remains unchanged.

ref = d2;

This changes the value of the object referred to by the reference, the value of d1 changes, the reference still refers to d1.

If you look at the assembly generated for your program, you'll see that both ptr and ref take up one word of memory: Compiler Explorer

ptr:
        .word   d1
ref:
        .word   d1

WOW! Now I've gone from not understanding to just plain stunned!

Thanks for trying to explain them. I think they're just to complicated for me to use.

-jim lee

PieterP:
The difference you're seeing is because the two lines do something completely different:

I know that "ref = d2" is the same as "d1 = d2", it was only included to prevent "ref" from being optimized away. If you remove "ref" from the example you will not free 2 bytes as you would by removing "ptr" and this suggests that "ref" is an alias for "d1" which takes up no memory because it only exists at compile time.

Danois90:
"ref" is an alias for "d1" which takes up no memory because it only exists at compile time.

That is simply not true, I showed you the assembly, it's there, and it takes up exactly one word of RAM, the same for ptr.

PieterP:
That is simply not true, I showed you the assembly, it's there, and it takes up exactly one word of RAM, the same for ptr.

The sketch in #43 uses 190 bytes of dynamic memory as is (compiled for UNO). Removing the "ref" declaration completely does not change the memory usage. Uncommenting the line "ptr = &d2" increases the memory usage to 192 bytes, because it prevents "ptr" from being optimized away. How comes that the "ref" declaration has no impact on memory usage? Are we using different compilers and/or settings?

void timesTwo(uint8_t &x);

void setup() {
  uint8_t a = 10;
  uint8_t b = 15;

  Serial.begin(115200);
  delay(1000);

  Serial.print("Old a = ");
  Serial.print(a);
  timesTwo(a);
  Serial.print(", New a = ");
  Serial.println(a);

  Serial.print("Old b = ");
  Serial.print(b);
  timesTwo(b);
  Serial.print(", New b = ");
  Serial.println(b);
}

void loop() {
}

void timesTwo(uint8_t &x) {
  x *= 2;
}

Output:

Old a = 10, New a = 20
Old b = 15, New b = 30

How can the reference parameter in the timesTwo() function be just a "Compile Time Alias" for BOTH variables 'a' and 'b' at the same time?

jimLee:
WOW! Now I've gone from not understanding to just plain stunned!

Thanks for trying to explain them. I think they're just to complicated for me to use.

If you can use pointers, you can use references :wink: In use, they are pretty much the same as a pointer constant with the exception they:

  • Auto reference (no need to use & when defining it)
  • Auto dereference (no need to use * when using it)
  • can't be null

septillion:
If you can use pointers, you can use references :wink: In use, they are pretty much the same as a pointer constant with the exception they:

  • Auto reference (no need to use & when defining it)
  • Auto dereference (no need to use * when using it)
  • can't be null
  • They are bound to the variable they reference only ONCE (when they are created). That binding can't be changed during the lifetime of the reference variable.

gfvalvo:
How can the reference parameter in the timesTwo() function be just a "Compile Time Alias" for BOTH variables 'a' and 'b' at the same time?

When "timesTwo(a)" is compiled, the compiler knows that "&x" is a reference to (or alias of) "a" - the same goes for "b" :slight_smile:

Danois90:
When "timesTwo(a)" is compiled, the compiler knows that "&x" is a reference to (or alias of) "a" - the same goes for "b" :slight_smile:

So, you're saying that 2 different version of the function get compiled taking up twice the required program memory?

gfvalvo:
So, you're saying that 2 different version of the function get compiled taking up twice the required program memory?

No, different addresses are manipulated by the same block of code - of course.

Danois90:
When "timesTwo(a)" is compiled, the compiler knows that "&x" is a reference to (or alias of) "a" - the same goes for "b" :slight_smile:

How do you know that "the compiler knows"? I posted a Compiler Explorer link in one of my previous posts, did you look at it?

PieterP:
How do you know that "the compiler knows"? I posted a Compiler Explorer link in one of my previous posts, did you look at it?

I guess the compiler has to know this in order to generate the binary.. I did look at your link, but I think it's not very usefull. I just made this minor C program (compiled to 64bit):

#include <stdio.h>

void test(char &x)
{
	printf("%ld\n", (long int)&x);
	x *= 2;
}

int main()
{
	char c = 54;
	char &ref = c;
	printf("%ld\n%ld\n", (long int)&c, (long int)&ref);
	test(c);
	return 0;
}

And the output are three identical numbers which means that "ref" and "x" are aliases for "c" since they all exists in the same memory location.

EDIT: Extended the example with "test(&)".

Danois90:
And the output are three identical numbers which means that "ref" and "x" are aliases for "c" since they all exists in the same memory location.

I don't understand what you mean. Of course they print the same number, they are all pointers to "c".

PieterP:
I don't understand what you mean. Of course they print the same number, they are all pointers to "c".

Not pointers but references. Please explain why a reference does not occupy any memory like a pointer does, and proof it with a compilable sketch?

Danois90:
Please explain why a reference does not occupy any memory like a pointer does, and proof it with a compilable sketch?

They do occupy the same memory as a pointer.
A reference is a pointer with syntactic sugar on top. References take up the same amount of memory as pointers, they are passed as pointers to functions, and they are stored as pointers.

The reason that the code you posted show different memory usage, is because you are using the pointer and the references differently, as discussed before.

Here's an example of passing references and pointers to functions, and saving them in a struct. As you can see, the assembly generated for pointers is exactly the same, and the occupy the same amount of space.

__SP_H__ = 0x3e ; high byte of the stack pointer
__SP_L__ = 0x3d ; low byte of the stack pointer
__SREG__ = 0x3f
__tmp_reg__ = 0

; struct S {
;     S(char val, char &ref, char *ptr) __attribute__((noinline))
;     : val(val), 
;       ref(ref), 
;       ptr(ptr) {}
;     char val, &ref, *ptr;
; };

S::S(char, char&, char*):
        ; S::S(S *this, char val, char &ref, char *ptr)
        ;   this is passed in r24-r25
        ;   val  is passed in r22
        ;   ref  is passed in r20-r21
        ;   ptr  is passed in r18-r19
        ; 
        movw r30,r24  ; store the this pointer in r30-r31 (Z-pointer)
        st Z,r22      ; store val in this[0]
        std Z+2,r21   ; store the second byte of ref in this[2]
        std Z+1,r20   ; store the first byte of ref in this[1]
        std Z+4,r19   ; store the second byte of ptr in this[4]
        std Z+3,r18   ; store the first byte of ref in this[3]
        ret

; void bar(char c) {
;     S s{c, c, &c};
;     foo(s);  // Don't optimize away s
; }

bar(char):
        ; bar(char c)
        ;   c is passed in r24
        ;
        push r28         ; r28-r29 are call-saved registers
        push r29
        rcall .          ; grow the stack by 3×2 bytes
        rcall .
        rcall .
        in r28,__SP_L__  ; read the stack pointer into r28-r29 (Y-pointer)
        in r29,__SP_H__
        mov r22,r24      ; move `c` into r22 as the second argument
        std Y+6,r24      ; store c on the stack (at the bottom)
        movw r18,r28     ; store the stack pointer (Y-pointer) in r18-r19
        subi r18,-6      ; add 6 to r18-r19 (bottom of stack)
        sbci r19,-1

                ; stack layout
                ;
                ; +---------+   ← bottom of stack (high address) (r18-r19)
                ; |   c     |
                ; +---------+
                ; |  s[4]   |
                ; + - - - - +
                ; |  s[3]   |
                ; + - - - - +
                ; |  s[2]   |
                ; + - - - - +
                ; |  s[1]   |
                ; + - - - - +
                ; |  s[0]   |
                ; +---------+   ← top of stack (low address) (r28-r29)

        movw r20,r18     ; move the bottom of the stack (pointer to `c`) into 
                         ; r20-r21 as the third argument (`ref`)
                         ; the fourth argument is already in r18-r19 (`ptr`)
                         ; the second argument is already in r22 (see above)
        movw r24,r28     ; move the top of the stack into r24-r25
                         ; as the first argument (`this`)
        adiw r24,1       ; increment r24-r25 to point to `s`

        call S::S(char, char&, char*)  ; call the constructor for S

        ; call foo
        movw r24,r28
        adiw r24,1
        call foo(S&)
        adiw r28,6

        ; restore the stack 
        in __tmp_reg__,__SREG__
        cli
        out __SP_H__,r29
        out __SREG__,__tmp_reg__
        out __SP_L__,r28
        pop r29
        pop r28
        ret

The pointer and the reference are stored in exactly the same manner:

This is the assembly generated for the constructor of S, that saves both the reference "ref" and the pointer "ptr" to the new struct.

[size=9pt]        std Z+2,r21   ; store the second byte of ref in this[2]
        std Z+1,r20   ; store the first byte of ref in this[1]
        std Z+4,r19   ; store the second byte of ptr in this[4]
        std Z+3,r18   ; store the first byte of ref in this[3][/size]

A pointer argument and a reference argument are passed in exactly the same manner, as the address of the variable they point/refer to:

This is the code that prepares to call the constructor of S, it places the arguments into the right registers (the "this" pointer to the new object goes into r24-r25, the value in r22, the reference in r20-r21, and the pointer in r18-r19.
As you can see, the compiler just copies the pointer argument to the reference argument, they are identical (movw r20,r18

[size=9pt]        in r28,__SP_L__  ; read the stack pointer into r28-r29 (Y-pointer)
        in r29,__SP_H__

        std Y+6,r24      ; store c on the stack (at the bottom) ("c" is in r24)
        movw r18,r28     ; store the stack pointer (Y-pointer) in r18-r19 (top of stack)
        subi r18,-6      ; add 6 to r18-r19 (bottom of stack)
        sbci r19,-1

                ; stack layout
                ;
                ; +---------+   ← bottom of stack (high address) (r18-r19)
                ; |   c     |
                ; +---------+
                ; |  s[4]   |
                ; + - - - - +
                ; |  s[3]   |
                ; + - - - - +
                ; |  s[2]   |
                ; + - - - - +
                ; |  s[1]   |
                ; + - - - - +
                ; |  s[0]   |
                ; +---------+   ← top of stack (low address) (r28-r29)

        movw r20,r18     ; move the bottom of the stack (pointer to `c`) into
                         ; r20-r21 as the third argument (`ref`)
                         ; the fourth argument is already in r18-r19 (`ptr`)[/size]

To get back to the example where you were wondering about the 2 bytes memory difference, if you use the pointer in the same way as the reference, the compiler performs the same optimizations, the generated code is identical, and so is the memory usage.
Replace "ptr = &d2" with "*ptr = d2". The latter is equivalent to "ref = d2".

"ptr = &d2" reassigns the pointer:

        ldi r24,lo8(d2)  ; load the address of d2
        ldi r25,hi8(d2)
        sts ptr+1,r25    ; store the address in ptr
        sts ptr,r24

"*ptr = d2" assigns the value of d2 to the pointee:

        lds r22,d2  ; load the value of d2
        sts d1,r22  ; store the value of d2 into d1

"ref = d2" assigns the value of d2 to the referenced value:

        lds r22,d2  ; load the value of d2
        sts d1,r22  ; store the value of d2 into d1

The latter two are identical, the ones you had in your program were not, hence the difference in binary size and RAM usage.

Sorry for the late answer! It seems that I cannot cause a reference to use any dynamic memory. I would like to share this information and at the very top I find:

Q: What is a reference?
A: An alias (an alternate name) for an object.

As long as no one posts any code where a reference cleary consumes the same dynamic memory as a pointer, I will continue to consider references as being aliasses.

Other links: Wikipedia and Stackoverflow.

Well, for me all this boils down to is :

How useful is a coding construct that is so vague, that it causes arguments as to what it is?

-jim lee

jimLee:
Well, for me all this boils down to is :

How useful is a coding construct that is so vague, that it causes arguments as to what it is?

-jim lee

You probably use References all the time without realizing it. Recommended practice for Arduino libraries is to pass arguments using References to functions if the function needs to modify the argument or it would be inefficient to place on the stack (i.e. large structs and objects). This is to shield fragile newbies from pointers.

Also, if you ever use operator overloading, then References are the only way it can be done.