7

Consider the following code:

#include <iostream>

struct Thing
{
    Thing(void)                       {std::cout << __PRETTY_FUNCTION__ << std::endl;}
    Thing(Thing const &)              = delete;
    Thing(Thing &&)                   = delete;
    Thing & operator =(Thing const &) = delete;
    Thing & operator =(Thing &&)      = delete;
};

int main()
{
    Thing thing{Thing{}};
}

I expect Thing thing{Thing{}}; statement to mean construction of temporary object of Thing class using default constructor and construction of thing object of Thing class using move constructor with just created temporary object as an argument. And I expect that this program to be considered ill-formed because it contains an invocation of deleted move constructor, even though it can be potentially elided. The class.copy.elision section of standard seems to demand this as well:

the selected constructor must be accessible even if the call is elided

The Wording for guaranteed copy elision through simplified value categories does not seem to allow it either.

However gcc 7.2 (and clang 4 as well, but not VS2017 which still does not support guaranteed copy elision) will compile this code just fine eliding move constructor call.

Which behavior would be correct in this case?

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
user7860670
  • 35,849
  • 4
  • 58
  • 84
  • Why would you expect a temporary here, but not in `Thing thing{f()};` where `f()` returns a `Thing` ? – Bo Persson Sep 06 '17 at 09:59
  • Also, I suspect that "accessible" means "not private". – Bo Persson Sep 06 '17 at 10:00
  • @BoPersson No, it also works with private constructors. – Hatted Rooster Sep 06 '17 at 10:00
  • @BoPersson I would expect `Thing thing{f()};` to mean invocation of move constructor with a temporary returned by `f()` call a an argument. Which will be hopefully elided avoiding overhead of creating temporary. – user7860670 Sep 06 '17 at 10:05
  • _The Wording for guaranteed copy elision through simplified value categories does not seem to allow it either_ It does, but surprisingly few people understand **how** (or they don't hurry to answer). The selected answer only shows the Example, but it is not a normative wording. And the normative wording in front of the Example doesn't guarantee that the whole `T(T(T()))` collapses into `()` or that the copy/move constructor is not selected by overload resolution. – Language Lawyer Jun 17 '20 at 23:53
  • BTW, that wording doesn't even seem apply to your case. – Language Lawyer Jun 18 '20 at 00:19

1 Answers1

12

It doesn't make an ill-formed program build. It gets rid of the reference to the deleted function entirely. The appropriate wording in the proposal is here:

[dcl.init] bullet 17.6

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example: T x = T(T(T())); calls the T default constructor to initialize x. ]

The example further strengthens this. Since it indicates the whole expression must collapse into a single default construction.

The thing to note is that the deleted function is never odr-used when the copies are elided due to value categories, so the program is not referring to it.

This is an important distinction, since the other form of copy elision still odr-uses the copy c'tor, as described here:

[basic.def.odr]/3

... A constructor selected to copy or move an object of class type is odr-used even if the call is actually elided by the implementation ([class.copy] ...

[class.copy] describes the other form of permissible (but not mandatory) copy-elision. Which, if we demonstrate with your class:

Thing foo() {
    Thing t;
    return t; // Can be elided according to [class.copy.elision] still odr-used
}

Should make the program ill-formed. And GCC complains about it as expected.


And by the way. If you think the previous example in the online compiler is a magicians trick, and GCC complains because it needs to call the move c'tor. Have a look at what happens when we supply a definition.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Thank you for detailed reply. I think the next (17.6.2) bullet is actually more interesting: "Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered."That is rvalue reference constructors are not considered when passing appropriate prvalue, so program seem to stay well-formed because deleted constructor was not considered. But it seems to be somewhat inconsistent with copy/move elision section requirement. – user7860670 Sep 06 '17 at 11:02
  • 1
    @VTT - The copy/move elision section is about a different thing, really. I think it's pertinent to note that while the proposal is colloquially for "mandatory copy/move elision", it uses the term "temporary materialization" instead. So essentially, it doesn't define a copy that *may* be elided. It simply delays the initialization to as late a stage as possible, so there is in fact no need for a copy. That's how the magic happens. – StoryTeller - Unslander Monica Sep 06 '17 at 11:05
  • 1
    The problem is that with deleted / not accessible constructors not considered this delayed initialization makes it possible to [pass objects around by value ultimately leading to object construction in places that would be otherwise prohibited to do so](http://coliru.stacked-crooked.com/a/e545a9ced7f47fdb). I can imagine some benefits (for example class encapsulating `mutex` and having a method directly returning `lock_guard` instead of returning a reference to `mutex` to construct `lock_guard` on the user side). But it still feels wrong. – user7860670 Sep 06 '17 at 11:22
  • 1
    @VTT - I get why it feels wrong. It's kind of a major paradigm shift. But as you said, it also as many benefits. – StoryTeller - Unslander Monica Sep 06 '17 at 11:27
  • 3
    @VTT: You're not passing objects around. You are passing prvalues around, which are *explicitly* not objects yet. [That's the trick that makes guaranteed elision work. A prvalue is not an object yet. It is an *initializer* for an object.](https://stackoverflow.com/questions/38043319/how-does-guaranteed-copy-elision-work/38043447#38043447) By redefining the meaning of prvalues, you remove the operations that would have been elided. – Nicol Bolas Sep 06 '17 at 14:43
  • _The appropriate wording in the proposal is here_ The initializer is not a prvalue expression but braced list, how is it appropriate? – Language Lawyer Jun 18 '20 at 00:08