Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it illegal to bind an r-value to a const l-value reference in special member functions?

For function parameters, it is possible to bind an r-value to an l-value const reference. However, this does not seem to apply to special member function like the copy-constructor, and copy-assignment operator in C++11 and C++14. Is there a motivation for this?

When using C++17, it is possible to copy-construct, but not copy assign, from an r-value. Is there a motivation why only the behavior for the copy-constructor was changed here?

All of this is demonstrated in the following example:

struct B {
 B() = default;
 B(B const&) = default;
 B(B&&) = delete;
 B& operator=(B const&) = default;
 B& operator=(B&&) = delete;
};

void bar(B const &) {}

int main() {
    bar(B{}); // does work
    B(B{}); // only works in C++17

    B b{};
    b = B{}; // doesn't work
}
like image 829
Arqubusier Avatar asked Aug 13 '20 07:08

Arqubusier


1 Answers

B(B{}); works since C++17 because of mandatory copy elision, the move-construction is omitted completely; the temporary object is initialized by the default constructor directly.

(emphasis mine)

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:

  • ...

  • In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:

      T x = T(T(f())); // only one call to default constructor of T, to initialize x
    

Note: the rule above does not specify an optimization: C++17 core language specification of prvalues and temporaries is fundamentally different from that of the earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing": prvalues are returned and used without ever materializing a temporary.

Before C++17 this is an optimization and B(B{}); is ill-formed.

This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed

bar(B{}); works because there's only one overlaod of bar which taking lvalue-reference to const, to which rvalues could be bound; this doesn't change since C++11.

b = B{}; doesn't work because the overloaded move assignment operator is selected; even it's marked as delete explicitly it still participates in overload resolution[1]. For bar if you add an overloading taking rvalue-reference as

void bar(B&&)=delete;

It'll be selected and cause the program ill-formed too.


[1] Note that it's not true for deleted implicitly declared move constructors, which are ignored by overload resolution. (since C++14)

The deleted implicitly-declared move constructor is ignored by overload resolution (otherwise it would prevent copy-initialization from rvalue).

like image 125
songyuanyao Avatar answered Nov 17 '22 13:11

songyuanyao