8

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
}
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
Arqubusier
  • 83
  • 3

1 Answers1

11

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

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • 2
    I would add that the other examples do not work because `delete` is applied after overload resolution selects the correct function even from deleted ones. Adding `void bar(B&&)=delete;` will also result into compilation errors. – Quimby Aug 13 '20 at 08:01
  • 1
    There's more than that. "A defaulted move constructor that is defined as deleted is ignored by overload resolution" (class.copy.ctor). Same about move assignment. It is not applicable here though because the move ctor is not defaulted (i.e. explicitly deleted). But if you have `struct C : B {}`, you can copy-construct and copy assign `C` from *any* rvalue reference (e.g. `C c; C cc = std::move(c);`.This I believe is also true for C++14. – n. m. could be an AI Aug 13 '20 at 08:28
  • `B(B{});` is not an initialization. Why do you cite the "rule" about copy elision in initialization? – Language Lawyer Aug 13 '20 at 13:37
  • @LanguageLawyer Why it not an initialization? Isn't it initializing an temporary object? And, what should it be? – songyuanyao Aug 13 '20 at 13:50
  • The quote says _... when **the initializer expression** is a prvalue ..._, AFAIK the term (is it even a term?) "initializer expression" is only used in context of declarations like `T x = T(T(f()));` in the quote (https://timsong-cpp.github.io/cppwp/n4659/dcl.init#17). `B(B{});` is _expression-statement_, not _declaration_. – Language Lawyer Aug 13 '20 at 14:24
  • @LanguageLawyer I take it as, `B(B{});` is initializing a temporary object with type `B`, the initializer is `B{}`, which is an prvalue of the same type `B`, as the effect the temporary object is initialized by the default constructor directly. – songyuanyao Aug 13 '20 at 14:28
  • `B(B{})` in `B(B{});` is a prvalue discarded-value expression and temporary materialization conversion applied to it. [conv.rval] says _This conversion initializes a temporary object of type T **from the prvalue** by evaluating the prvalue with the temporary object as its result object_. It doesn't really name the prvalue "initializer", but if we want to name it so, this reads to me that the initializer is `B(B{})`. Well, there is no big difference in why there are no (intermediate) temporary objects in declaration and when a temporary object is materialized in discarded-value expression... – Language Lawyer Aug 13 '20 at 14:33
  • ... but the actual wording ensuring the absence of excess temporary objects is in [expr]. Namely, since `B(B{})` is a function-style cast, the relevant wording starts from [\[expr.type.conv\]/2](https://timsong-cpp.github.io/cppwp/n4659/expr.type.conv#2). – Language Lawyer Aug 13 '20 at 14:37
  • @LanguageLawyer I'm not sure, but I think the effect (functional cast) is same as [direct initialization](https://en.cppreference.com/w/cpp/language/direct_initialization). – songyuanyao Aug 13 '20 at 14:43
  • I looked into my answers and... it is you who couldn't explain how copy-initialization becomes direct-initialization. – Language Lawyer Aug 13 '20 at 14:46
  • @LanguageLawyer I didn't say `B(B{})` is copy-initialization.. ? – songyuanyao Aug 13 '20 at 14:47
  • I meant [this answer](https://stackoverflow.com/a/44154472/). – Language Lawyer Aug 13 '20 at 14:50
  • @LanguageLawyer Because in that question `X a = X()` is copy-initialization? Sorry I can't get what you want to say now.. – songyuanyao Aug 13 '20 at 14:53
  • The question asks: why `explicit` default constructor is selected in the context of copy-initialization. And your answer doesn't seem to really explain this. _«Sorry I can't get what you want to say at all»_ I was gonna link my answer to confirm that you're right saying that _«the effect (functional cast) is same as direct initialization»_ and realized that you already know about that answer. – Language Lawyer Aug 13 '20 at 14:57
  • Yes, it is the same wording which guarantees the absence of excess temporary objects in _declaration_ `B b = B(B{});` and when the temporary materialization conversion is applied to the discarded-value expression `B(B{})`, I just wanted to point that the quote in this answer looks like it applies only to _declaration_ s, because the "term" "initializer expression" seems to be used only in that context. – Language Lawyer Aug 13 '20 at 15:02