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.