String arguments to functions - (String & msg) or (String msg)

Yes, I know, I shouldn't be using Strings, and I have successfully weaned myself off them in the most part (massive thanks to those here who have helped me) - it has been very painful! I was making really silly mistakes with pointers, like having global const char *, assigning it in a method to the argument of a method (sorry, function), and wondering why the value kept changing when I wasn't looking... Anyway, I digress.

I cannot do away with Strings entirely as I am using the PainlessMesh library which uses JSON Strings for messaging and eg has the following callback:

void receivedCallback(uint32_t from, String & msg);

So this is passing the address of the String object to the method, right? And I can use it to change the value of msg, eg

msg = "new value";

But I also had, previously, a method I wrote as

void handleSerialData(String msg);

Here, I can use msg as is, and also change the value of msg with

msg = "new value";

So what am I actually passing into the function here? What is happening 'under the hood'? In the second case, what is 'msg'? How does it work? Should I always use the first, passing the address? why? etc.

Many thanks,
Joe

1 Like

In the first case, you're passing a reference to msg to the function. This doesn't create a copy, but refers to the string you pass in as an argument.

In the second case, you're passing the string by value. This creates a local copy of the string you pass as an argument. Inside of the function, you can only change this local copy, not the original string that was passed in. After the function exits, the local copy is destroyed, and any changes to it are lost.

For example:

void function1(String msg) { // string by value → create local copy
    msg = "function1";       // only changes local copy
}

void function2(String &msg) { // string by reference → refers to original msg
    msg = "function2";        // changes original message
}

void setup() {
    Serial.begin(115200);
    while (!Serial);
    String msg = "setup";
    Serial.println(msg); // "setup"
    function1(msg);      // by value (msg is copied)
    Serial.println(msg); // "setup" (msg didn't change)
    function2(msg);      // by reference (msg is not copied)
    Serial.println(msg); // "function2" (msg changed)
}

void loop() {}

jeronimojoe:
So this is passing the address of the String object to the method, right?

Under the hood, it is passing the address of the String object to the method, yes. But you shouldn't think of it that way, it passes a reference to the original String, which just behaves as an alias for the original String.
The ampersand (&) here declares the type of "msg": it means "reference to" (the type of msg is "reference to String").
Not to be confused with the "address-of" operator, which is also denoted by an ampersand, but used in different circumstances.

Pieter

Thanks, a very clear reply - until this bit!

"The ampersand (&) here declares the type of "msg": it means "reference to" (the type of msg is "reference to String").
Not to be confused with the "address-of" operator, which is also denoted by an ampersand, but used in different circumstances."

I understand passing by reference; but when would & be used as an "address-of" operator; and more importantly, how will I know that is what it is being used for? And why - is it so that you can access the value of the pointer?

Consider the following snippet:

    int i    = 42;
    int* ptr = &i;
    int& ref = i;

The first line declares an integer "i".

The second line declares a variable "ptr" of type "int " or "pointer to int". A pointer variable stores the address of another variable. "ptr" is initialized with the "address of" i. The ampersand is used as the address-of operator.
The asterisk (
) is part of the type specifier.

The third line declares a variable "ref" of type "int &" or "reference to int", and is initialized as an alias to "i" ("ref" is bound to "i"). Here, the ampersand is part of the type specifier, like the asterisk on line 2.

The same "ambiguity" exists for the asterisk:

    int j = *ptr;

In this case, the asterisk is used as the dereference operator, to access the variable that "ptr" points to.

In practice, there's no real problem, because the context makes clear how the ampersand (or asterisk) is used.
If it's part of a type specifier, it declares a reference type (or pointer type), if it's used in an expression, it's used as an address-of operator (or dereference operator).

It is common practice to write int *ptr = ..., but this is equivalent to int* ptr = ....

1 Like

PieterP:
It is common practice to write int *ptr = ..., but this is equivalent to int* ptr = ....

It actually depends. I'm used to use int *ptr myself. But the new codebase I'm working on requires int* ptr.

Additionally, if you have:

void mess(String &msg) //pass by reference

and

void mess(String *msg) //pass by pointer

Most of the time you would want to use the first case because it is safer.

PieterP:
Consider the following snippet:

    int i    = 42;

int* ptr = &i;
    int& ref = i;



The first line declares an integer "i".

The second line declares a variable "ptr" of type "int *" or "pointer to int". A pointer variable stores the address of another variable. ...
The third line declares a variable "ref" of type "int &" or "reference to int", and is initialized as an alias to "i" ("ref" is *bound* to "i").

OK, I think I get this. But am I right in thinking that ptr and ref actually hold the same value, that is, the address of i?

jeronimojoe:
OK, I think I get this. But am I right in thinking that ptr and ref actually hold the same value, that is, the address of i?

If you were to inspect the memory, you would be right, but there are important differences between pointers and references.

Most importantly, a reference cannot be re-bound to another object. It can only be bound once. As soon as it is bound to an object, a reference basically becomes indistinguishable from that object.

int i = 1;
int j = 2;
int &ref = i;
ref = j;
// i is now 2, ref still refers to i

Whoa! Now this is the stuff which needs explaining, and I have never seen explained before.

So to test if I understand, lets try this:

int i = 1;
int j = 2;
int* ref  = &i;
ref = j;
[\code]

So.... now let me think - is it: i is still 1, j is still 2, but ref is now a pointer to j?

What happened when you tried it? Be sure to turn up your compiler warnings in File --> Preferences.

jeronimojoe:
So.... now let me think - is it: i is still 1, j is still 2, but ref is now a pointer to j?

Almost, you need another address-of to make it work.

int i = 1;
int j = 2;
int* ptr  = &i;
ptr = &j;
// i is still 1, j is still 2, ptr first points to i, and then points to j.

Writing equivalent code using references would not be possible, because you can't first have it refer to i and then to j.

On the other hand, you can use pointers to get the same behavior as the example I posted in reply #6:

int i = 1;
int j = 2;
int &ref = i;
ref = j;
// i is now 2, ref still refers to i
int i = 1;
int j = 2;
int *ptr = &i;
*ptr = j;
// i is now 2, ptr still points to i

@ PieterP
Yep, I can follow both your examples and make sense of them sith only a minor headache, thank you so much!

To tie up a loose end, can we go back to my imperfect example:

int i = 1;
int j = 2;
int* ref  = &i;
ref = j;
[\code]

So what is ref now? I assigned the value of j to it, ie an int value of 2, but it is of type int*, a pointer to an int, so .... err, it now 'points' to a (probably non-existent) int at memory address 2?

jeronimojoe:
To tie up a loose end, can we go back to my imperfect example:

int i = 1;

int j = 2;
int* ref  = &i;
ref = j;




So what is ref now? I assigned the value of j to it, ie an int value of 2, but it is of type int*, a pointer to an int, so .... err, it now 'points' to a (probably non-existent) int at memory address 2?[/code]

That code is invalid:

error: invalid conversion from 'int' to 'int*' [-fpermissive]
   ref = j;
         ^

You can make this compile by explicitly reinterpreting the integer as a pointer:

ref = reinterpret_cast<int *>(j); // undefined behavior, don't do this

Ref points to an integer at address 2. However, in that case, the code is most likely meaningless (and invokes undefined behavior), because there is no int at address 2.

On a small microcontroller, similar constructs can be useful when writing the hardware support libraries. Certain registers and peripherals are memory-mapped, and they have a fixed address.
For example, the PINB input register (used for digitalRead) on an Arduino UNO is defined (inside of the compiler's header files) as

#define __SFR_OFFSET 0x20
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
#define PINB _SFR_IO8(0x03)

This is basically converts the integer 0x23 to a pointer. If you look in the ATmega328P datasheet, you'll discover that 0x23 is in fact the hardware address of the PINB register.
(This code is C code, not C++, so it uses C-style casts, which are equivalent to reinterpret_cast in this case.)

In most modern CPUs with an MMU (all modern ARM Cortex-A, Intel and AMD CPUs), this is not possible, because user applications run in a virtual address space with address space layout randomization (ASLR). This means that absolute addresses are completely meaningless, as addresses differ from run to run, and do not correspond with the underlying hardware addresses. Therefore, converting a constant integer to a pointer is meaningless as well.
(If you want access to an absolute hardware address on such systems, you'll have to use the mmap syscall, or write the code that uses the hardware address in a kernel module instead of in the user space application.)
The main takeaway is that on moderns systems, the addresses stored in a pointer variable do not correspond to a physical address directly.

In general, converting between pointers and integers is bad practice. There were a lot of compatibility issues when moving from 32-bit to 64-bit systems, because people were storing pointers as integers. On most 64-bit platforms, integers are still 32-bit, and pointers are 64-bit, so they suddenly no longer fit.
There are even architectures where pointers are saved in a different set of CPU registers than integers, and where implicitly reinterpreting an int as a pointer would read a value from the wrong register, crashing the program.

If you really need a pointer as an integer, use the uintptr_t type and reinterpret_cast: reinterpret_cast conversion - cppreference.com (see point 2).
Even then, it undermines the type system, and should be avoided.

arduino_new:
It actually depends. I'm used to use int *ptr myself. But the new codebase I'm working on requires

"int* ptr" and "int *ptr" are 100% functionally identical. It is impossible for ANY "codebase" to "require" one or the other.

Regards,
Ray L.

@PieterP Gosh, thank you, I am somewhat awestruck. FWIW, I don't know any C++, C-style casts are all I know; and don't worry, I have no intention of casting integers to pointers, it was just a 'what if...' scenario!!

However, I do have one more question on this topic arising from de-Stringing my code, which is cracking along now thanks to your help. I am now passing char[] as argument instead of String, and it seems I have to do this as char * msg or const char * msg; I understand that this is a pointer to the first element of the char []. So why do we use a pointer here and a reference with a String? (Something to with a String being an object, I am guessing?)

And why do I not have to dereference it as in your example in message #3 above:

if(*msg[0] == 0){
}

Megathanks,
joe

An Arduino String (capital S) is an object that owns the storage for a heap-allocated character array.
When you write String s = "Hello, world";, the String allocates memory, and copies all characters of the string literal into that memory. When s is destroyed, its memory is deallocated. When you create a copy of a String variable, the character array it owns is copied as well. This requires an extra allocation and as a result, there are now two copies of the same String in memory.
Most of the time, this copy is redundant and just a waste of memory. That's why it's better to pass it by reference rather than by value.

C-strings are different, they are just pointers to a character array that was allocated somewhere else.
When you write const char *s = "Hello, world";, the compiler stores the string literal somewhere in a character array in the static memory section of the program. The variable s is then just a pointer to that array. It doesn't own the array, it just points to it (to its first character). Copying such a pointer is very cheap, so you can easily pass the pointer as a function argument.

Most functions that manipulate C-strings assume that the pointer points to an array of characters, not to a single character. The end of the array is indicated by a special NULL character '\0'.

void print(const char *string) {
  while (*string != '\0') { // as long as the current character is not NULL
    Serial.print(*string); // print the character
    ++string; // advance the pointer to point to the next character in the array
  }
}

void setup() {
  Serial.begin(115200);
  print("Hello");
}

When entering the print function, string points to the beginning of the character array:

H  e  l  l  o \0
⇡
string

This means that dereferencing string using *string or string[0] (both are equivalent) will yield the character 'H'.
'H' is not null, so we enter the loop, print 'H', and advance the pointer. The pointer string now points to the second element of the character array:

H  e  l  l  o \0
   ⇡
   string

*string now yields the character 'e', and so on, until string points to the null character '\0' and the condition of the while loop is no longer satisfied.

Note that you have to use only one method of dereferencing at a time, either *string or string[0], not both.
The brackets [] (subscript operator) are just syntactic sugar for a dereference operator: Member access operators - cppreference.com

The built-in subscript expression E1[E2] is exactly identical to the expression *(E1 + E2)

This means that string[0] is equivalent to *(string + 0) or just *string.

Why are pointers used for C-strings? I don't know exactly, but C++ evolved from C, and C didn't have references. It also couldn't pass arrays to functions, C-arrays automatically decay to pointers when passed as an argument. So the natural way to pass an array of characters to a function was to use a pointer (char *).

It's not allowed to write to the memory of a string literal, so the pointer must be read-only, that's why const char * is often used instead of just char *. It's good practice to always use const if your function is not going to write to the string argument.

For example, the standard function strcpy has the signature char *strcpy( char *dest, const char *src ). This indicates that the function will only read from src, but it will write to dest. The compiler will enforce this (e.g. you cannot pass a read-only string literal as the destination of a copy).

(The print function above was an example, you can just pass string to the Serial.print() function directly, and it will know what to do with it.)