jwimberley
jwimberley

Reputation: 1748

G++ 7.1.0 onwards supports guaranteed copy elision in this constructor, but Clang++ 4.0 onwards do not

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

Answers (2)

NathanOliver
NathanOliver

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

eerorika
eerorika

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

Related Questions