I’m sure many are familiar with the terms pass-by-reference and pass-by-value. In pass-by-reference a reference to the original value is passed into a function, which potentially allows the function to modify the value. In pass-by-value the function instead receives a copy of the original value. C++ has pass-by-value semantics by default (except, arguably, for arrays) but function parameters can be explicitly marked as being pass-by-reference, with the ‘&’ modifier.
Today, I learned that C++ will in some circumstances pass by reference to a (temporary) copy.
Interjection: I said “copy”, but actually the temporary object will have a different type. Technically, it is a temporary initialised using the original value, not a copy of the original value.
Consider the following program:
#include <iostream>
void foo(void * const &p)
{
std::cout << "foo, &p = " << &p << std::endl;
}
int main (int argc, char **argv)
{
int * argcp = &argc;
std::cout << "main, &argcp = " << &argcp << std::endl;
foo(argcp);
foo(argcp);
return 0;
}
What should the output be? Naively, I expected it to print the same pointer value three times. Instead, it prints this:
main, &argcp = 0x7ffc247c9af8 foo, &p = 0x7ffc247c9b00 foo, &p = 0x7ffc247c9b08
Why? It turns out that what we end up passing is a reference to a temporary, because the pointer types aren’t compatible. That is, a “void * &” cannot be a reference to an “int *” variable (essentially for the same reason that, for example, a “float &” cannot be a reference to a “double” value). Because the parameter is tagged as const, it is safe to instead pass a temporary initialised with the value of of the argument – the value can’t be changed by the reference, so there won’t be a problem with such changes being lost due to them only affecting the temporary.
I can see some cases where this might cause an issue, though, and I was a bit surprised to find that C++ will do this “conversion” automatically. Perhaps it allows for various conveniences that wouldn’t otherwise be possible; for instance, it means that I can choose to change any function parameter type to a const reference and all existing calls will still be valid.
The same thing happens with a “Base * const &” and “Derived *” in place of “void * const &” / “int *”, and for any types which offer conversion, eg:
#include <iostream>
class A {};
class B
{
public:
operator A()
{
return A();
}
};
void foo(A const &p)
{
std::cout << "foo, &p = " << &p << std::endl;
}
int main (int argc, char **argv)
{
B b;
std::cout << "main, &b = " << &b << std::endl;
foo(b);
foo(b);
return 0;
}
Note this last example is not passing pointers, but (references to) object themselves.
Takeaway thoughts:
- Storing the address of a parameter received by const reference is probably unwise; it may refer to a temporary.
- Similarly, storing a reference to the received parameter indirectly could cause problems.
- In general, you cannot assume that the pointer object referred to by a const-reference parameter is the one actually passed as an argument, and it may not exist once the function returns.
Yes, allowing temporaries is extremely useful behavior but also potentially dangerous. The intent is for f(T const&) to be as close a possible to f(T) in term of allowed arguments; the choice between f(T) and f(T const&) is an optimisation issue (less copying vs. aliasing), as both represent IN arguments (in Ada terms); f(T &) represents an IN OUT argument.
“Storing the address of a parameter received by const reference is probably unwise;”
You probably don’t ever want to store a pointer to a pass by reference parameter, be it const or non const. The caller won’t typically expect called function that don’t take pointers to keep pointers around.