Reputation: 1748
I've been trying to produce a class with a member that is an std::array of a non-copyable type, that I need to initialize in the constructor. I had thought, referring to answers on this SO question, that the following would work:
#include <array>
#include <utility>
class Foo {
public:
Foo() {}
Foo(const Foo& rhs) = delete;
Foo(Foo&& rhs) = delete;
};
template<size_t BufferSize>
class FooBuffer {
public:
template<size_t... Is>
FooBuffer(std::index_sequence<Is...>)
: _buffer({{(static_cast<void>(Is), Foo{})...}})
{
}
FooBuffer() : FooBuffer(std::make_index_sequence<BufferSize>{}) {}
private:
std::array<Foo,BufferSize> _buffer;
};
using namespace std;
int main(int, char **) {
FooBuffer<10> foo;
}
And so it does, from GCC version 7.1.0 onwards with the C++17 or C++17(GNU) flags, according to Wandbox:
link to working compilation under GCC 7.1.0
However, though guaranteed copy elision support is listed for Clang++ from version 4 onwards, I can find no version that accepts the above code, up through the 9.0 or the current HEAD:
link to compiler error under Clang
The error that is yielded relates to the non-copy-constructibility of the array _buffer
:
prog.cc:18:5: error: call to implicitly-deleted copy constructor of 'std::array<Foo, 10UL>'
: _buffer({{(static_cast<void>(Is), Foo{})...}})
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
prog.cc:22:16: note: in instantiation of function template specialization 'FooBuffer<10>::FooBuffer<0, 1, 2, 3, 4, 5, 6, 7, 8, 9>' requested here
FooBuffer() : FooBuffer(std::make_index_sequence<BufferSize>{}) {}
^
prog.cc:32:16: note: in instantiation of member function 'FooBuffer<10>::FooBuffer' requested here
FooBuffer<10> foo;
^
/opt/wandbox/clang-head/include/c++/v1/array:143:9: note: copy constructor of 'array<Foo, 10>' is implicitly deleted because field '__elems_' has a deleted copy constructor
_Tp __elems_[_Size];
^
prog.cc:8:2: note: 'Foo' has been explicitly marked deleted here
Foo(const Foo& rhs) = delete;
^
1 error generated.
Is this a disagreement on the implementation of guaranteed copy elision between the compilers? Or, I've seen indications here on SO that guaranteed copy elision cannot be relied on and is entire optional for the compiler. Does that apply here, or only in cases where there is a copy constructor and GCE is not required for correct code?
Upvotes: 1
Views: 178
Reputation: 180990
When you do
_buffer({{(static_cast<void>(Is), Foo{})...}})
The {(static_cast<void>(Is), Foo{})...}
part builds a braced-init-list for an object. The outer {}
is the braces for creating a the object the braced-init-list refers to. Since none of these list has a type the compiler has to enumerate the constructors of _buffer
to find out what to call. When the compiler does this all that is found are the implicitly generated copy and move constructors. Since Foo
is not copy/move-able those are implicitly deleted which means there is no constructor you can call.
If you switch to
_buffer{(static_cast<void>(Is), Foo{})...}
Then you have direct initialization of _buffer
and this is guaranteed to work since the Foo{}
prvalues are not copied but created directly in place.
You could also switch to using
_buffer(std::array<Foo,BufferSize>{{(static_cast<void>(Is), Foo{})...}})
and this will work because now you do have a prvalue of type std::array<Foo,BufferSize>
and instead of being copied it is directly initialized in _buffer
.
The pertinent standardese is found in [dcl.init]/17
- 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.
emphasis mine
Since a braced-init-list has no type, it does not qualify for the above bullet point so no guaranteed copy elision.
Upvotes: 3
Reputation: 238441
T(args...)
is direct initialisation. For direct initialisation of a class type, copy elision is guaranteed only when the argument is of that class type. But in your example, the argument is a braced-init-list.
GCC's failure to diagnose the issue is a violation of the standard.
This can be fixed by explictly creating a temporary, that will not be materialised (as perverse as it may seem):
: _buffer(std::array{(static_cast<void>(Is), Foo{})...})
Or simply by using list initialisation:
_buffer{(static_cast<void>(Is), Foo{})...}
Upvotes: 1