The Unterminated String

Embedded Things and Software Stuff

How Does Boost Move Emulation Work? (Addendum: Temporarys, Rvalues, Const and Confusion)

Posted at — Oct 4, 2020

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).

The Omission - Modifying an Rvalue

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)

A Fresh Example

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

Temporary’s Are Const?

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:

But 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).

Relevant Sections from the C++03ish Standard

The following are excerpts from: Working Draft, Standard for Programming Language C++, N1905=05-0165, 2005-10-19.

What is an rvalue?

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)).”

References to rvalues

Section [dcl.init.ref] 8.5.3.5 [non-lvalues, among other conditions, can only be bound to] “non-volatile const type”.

When can I modify an rvalue?

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 ]”

What is the lifetime of a temporary?

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 […].