Member initialization for non-copyable variable in C++17
Asked Answered
M

2

17

When performing member initialization for non-copyable variable (such as std::atomic<int>), it's required to use direct-initialization rather than copy-initialization according to answer here. However when I turn on -std=c++17 in g++ 7.4.0, it seems that the latter one also works well.

#include <atomic>

class A {
    std::atomic<int> a = 0;     // copy-initialization
    std::atomic<int> b{0};      // direct-initialization
};
$ g++ -c atomic.cc -std=c++11    // or c++14
atomic.cc:4:26: error: use of deleted function ‘std::atomic<int>::atomic(const std::atomic<int>&)’
     std::atomic<int> a = 0;     // copy-initialization

$ g++ -c atomic.cc -std=c++17
// no error

It also failed when compiling with g++ 6.5.0 even with -std=c++17. Which one is correct here?

Mythomania answered 30/9, 2019 at 8:44 Comment(1)
Possible duplicate ?: #1051879Severini
K
16

Behavior changed since C++17, which requires compilers to omit the copy/move construction in std::atomic<int> a = 0;, i.e. guaranteed copy elision.

(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, as the language rules ensure that no copy/move operation takes place, even conceptually:

In details, std::atomic<int> a = 0; performs copy initialization:

If T is a class type, and the cv-unqualified version of the type of other is not T or derived from T, or if T is non-class type, but the type of other is a class type, user-defined conversion sequences that can convert from the type of other to T (or to a type derived from T if T is a class type and a conversion function is available) are examined and the best one is selected through overload resolution. The result of the conversion, which is a prvalue temporary (until C++17) prvalue expression (since C++17) if a converting constructor was used, is then used to direct-initialize the object.

and

(emphasis mine)

if T is a class type and the initializer is a prvalue expression whose cv-unqualified type is the same class as T, the initializer expression itself, rather than a temporary materialized from it, is used to initialize the destination object

That means a is initialized from 0 directly, there's no temporary to be constructed and then no longer a temporary to copy/move from.

Before C++17, in concept std::atomic<int> a = 0; requires a temporary std::atomic to be constructed from 0, then the temporary is used to copy-construct a.

Even copy elision is allowed before C++17, it's considered as an optimization:

(emphasis mine)

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:

That's why gcc triggers diagnostic in pre-c++17 mode for std::atomic<int> a = 0; .

(emphasis mine)

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.

BTW: I suppose there was a bug in g++ 6.5.0 with -std=c++17; and it has been fixed in later version.

Kho answered 30/9, 2019 at 8:49 Comment(8)
I think you somehow misunderstood what OP is aksing. OP isn't asking for c++17 vs. pre-c++17.Severini
@Severini I think OP's asking why the copy initialization works with -std=c++17 (but not -std=c++11 or -std=c++14, and I tried to explain why it should work with C++17.Kho
Exactly, I think this answer is more right than the other one from @Severini (as it stands currently)Ashwell
@MaxLanghof Except for gcc 6.5.0 with -std=c++17 not accepting the code when it should. But only because its C++17 support was still partial and mandatory copy elision not implemented yet.Opprobrious
@uneven_mark Ok that's my failure. I somehow missed that other compiler version.Scrawly
@MaxLanghof I added that c++17 is required to my answer - I think that should be enough to clear up any remaining confusion.Severini
@MaxLanghof if that is the case/answer, isn't the question a duplicate of #1051879 ?Severini
Different cpp standard and g++ version cause my confusion here. When asking this question I have no idea that C++17 make this elision mandatory nor old g++ has bug. Thanks for everyone's answers/comments!Mythomania
S
3

Which one is correct here?

The 7.4.0 is correct. The copy can be elided for this case which is why it is Ok. (although this requires ).

(see https://en.cppreference.com/w/cpp/language/copy_initialization for more details)

Severini answered 30/9, 2019 at 8:49 Comment(8)
Isn't the rule something like: Even if it can be elided the behaviour should be as-if there was no elision performed and that should be legit. Then elision happens if the compiler can figure it out. (Sorry I don't have the std at hand just now). So maybe it's not that it's elided but some rule change (around that) for C++17 that has caused it to succeed there ?Ashwell
@Ashwell It's likely just a bug in the earlier version (these types of bugs in c++ implementations are unfortunately somewhat common) - see en.cppreference.com/w/cpp/language/copy_initializationSeverini
See this: en.cppreference.com/w/cpp/language/copy_elision : "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:"Ashwell
@Ashwell that's also stated in the link I provided to copy initializationSeverini
Ah i missed that edit refresh of yours. Is that not in contradiction to your answer then ? That it can be elided but the compiler must process it as if it wasn't and that should be legit. If the copy ctor is deleted then that is not legit hence simply stating it's elided and so OK doesn't sound right. Is it something to do with a newer rule around C++17 that's changed this behaviour (likely) ? Could you expand your answer to show why does it not contradict what's written in the link you gave ? Or am I missing something obvious you are wanting to say ?Ashwell
@Ashwell I can expand the answer for sure and will do so since it's requested (I just don't have time right now). For now, I really just try to keep things simple since OP hasn't tagged eg. "Language lawyer" or anything like that (and like I said it's just a bug in the old version which is causing OPs confusion).Severini
@Ashwell It is indeed a change in C++17 that makes this well-formed (see the other answer): Instead of being a conceptual copy (or move) of a temporary which could be elided, it is simply initialization now and no temporary is ever materialized (even conceptually).Scrawly
The OP has said that while using C++17 it's fine otherwise it's not. The copy can also be elided in earlier versions. So the failure is not just due to elision which your answer seems to suggest (as i quoted above, the pre-c++17 compiler must still ensure that the code is legit if it didn't perform the elision). It seems to be due to a rule change in C++17 which the other answer from @Kho tries to explain. Some explanation would def. helpAshwell

© 2022 - 2024 — McMap. All rights reserved.