I had cause to reread my initial write up of Boost move emulation and noticed a glaring omission. Since this had me scratching my head for a while, I figured it was worthy of a follow up post. Admittedly, it appears that much of my confusion stemmed from a misunderstanding I had with regard to why rvalues can only be bound to a const &
(in C++03).
In the original post, I used the class move_only_class
to provide a simplified example of how Boost move works.
The following code was used to illustrate how the class can move from an rvalue but not an lvalue:
move_only_class b(1);
b = move_only_class(100); // rvalue - can move
move_only_class a;
move_only_class b = a; // lvalue - compiler error
The reason for the compile error in the lvalue case is that the non-const
copy assignment operator is private
. Since this is clearly what the above code is trying to call the compiler is rightfully unhappy.
For the rvalue case, the private copy assignment operator cannot be a candidate for this call. An rvalue can only be bound to a const &
. The compiler is free to look for viable alternatives and finds the conversion function.
Case closed? I originally thought so, but when I looked at it with a fresh pair of eyes I became interested in how the conversion function facilitates passing a temporary to a function which takes an lvalue
:
move_only_class &
operator=(allow_move<move_only_class> &other)
The following is a cut down example which demonstrates this interesting use of the conversion operator:
#include <iostream>
template <typename T> struct derived : public T
{
~derived()
{
std::cout << "destructor derived\n";
}
};
struct base
{
operator derived<base> &()
{
std::cout << "conversion\n";
return *reinterpret_cast<derived<base> *>
(this);
}
~base()
{
std::cout << "destructor base\n";
}
};
int main(void)
{
std::cout << "before &=temp\n";
derived<base> &ref_derived = base();
std::cout << "after &=temp\n";
return 0;
}
In the above, the following line binds a non-const
reference to a temporary:
derived<base> &ref_derived = base();
The output of this program when executed is shown below.
The compiler is happily calling the conversion function and allowing the temporary to be bound to a non-const
reference.
(Unsurprisingly, the temporary object is destroyed before the final line is printed leaving a dangling reference).
before &=temp
conversion
destructor base
after &=temp
Through this mechanism we’re clearly able to get something the compiler thinks is an lvalue from a temporary, even going as far as to modify it in the move_only_class
example. This surely must be illegal? At least that was my knee jerk reaction.
So I consulted an old draft copy of the standard (I wouldn’t be relying on Boost move if I had access to a newer compiler). As best I can tell this is all legal, albeit in a roundabout way. The relevant references that led me to this conclusion are included at the bottom of this post for those who are curious.
After reflecting on why I thought this code wouldn’t be legal I think on some level I’d developed the idea that a temporary / rvalue should not be modified. I believe my confusion stemmed from muddling the following related rules and best practises:
const &
const
objects should not be modifiedconst &
to a temporary will extend the temporary’s lifetimeBut of course, temporary variables are modifiable. The conversion function I wrote above is non-const
and is happily invoked by the compiler. A more direct example:
#include <string>
#include <iostream>
int main()
{
std::cout << std::string("hello").append("!");
return 0;
}
So why does the standard stipulate that rvalues must be bound to a const &
?
Like the majority of people, when presented with a difficult question, I reached for Google. This led to the following Stack Overflow answer, which presents a simple but compelling answer:
https://stackoverflow.com/a/51339998/1510029
This design choice prevents the user from mistakenly thinking they have modified a certain object, when in fact they’ve only modified a temporary. The example provided in the post uses an implicit conversion from int
to double
as the source of the temporary.
Indeed, with the advent of rvalue references (&&
) in C++11, rvalues can be bound to &&
in addition to const &
. This allows the programmer to explicitly indicate when they are happy to accept a temporary or not. Temporary’s bound to &&
can of course be modified. (And either const &
or &&
will extend the lifetime of the temporary).
The following are excerpts from: Working Draft, Standard for Programming Language C++, N1905=05-0165, 2005-10-19.
Section [basic.lval] 3.10.5 “The result of calling a function that does not return a reference is an rvalue. User defined operators are functions, and whether such operators expect or yield lvalues is determined by their parameter and return types”
Section [basic.lval] 3.10.6 “An expression which holds a temporary object resulting from a cast to a nonreference type is an rvalue (this includes the explicit creation of an object using functional notation (5.2.3)).”
Section [dcl.init.ref] 8.5.3.5 [non-lvalues, among other conditions, can only be bound to] “non-volatile const type”.
Section [basic.lval] 3.10.10 “An lvalue for an object is necessary in order to modify the object except that an rvalue of class type can also be used to modify its referent under certain circumstances. [ Example: a member function called for an object (9.3) can modify the object. — end example ]”
Section [class.temporary] 12.2.5 The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference […].