Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Initialization of an array of non-moveable objects: why does such code fail to compile on GCC?

Here is the code example where Test is a non-copyable and non-moveable class with some virtual members and a user-defined constructor, and B is a class that contains a raw (C-style) array of Test objects:

class Test
{
public:
    Test() = delete;

    Test(const Test&) = delete;
    Test(Test&&) = delete;
    Test& operator=(const Test&) = delete;
    Test& operator=(Test&&) = delete;

    Test(int a, int b) : a_(a), b_(b) {}
    virtual ~Test() {}

    int a_;
    int b_;
};

//----------------

class B
{
public:
/*(1)*/ B() : test_{{1, 2}, {3, 4}} {} // Does not compile on GCC, but compiles on Clang and MSVC

private:
        Test test_[2];
};

//---------------- 

int main()
{
        B b;
/*(2)*/ Test test[2] = {{1, 2}, {3, 4}}; // Successfully compiles on GCC, Clang and MSVC
}

I want to initialize B's internal array test_ using the braced initialization syntax (line /*1*/), so that each of the two Test objects would be constructed in-place, without needing to create a temporary and then move it.

On Clang and MSVC, this code compiles without warnings.

But GCC's behavior confuses me: it fails to compile the line /*1*/, while successfully compiling the line /*2*/, where I am using the same syntax to initialize a local array. Yet for compiling the first line, it still requires the deleted move constructor of class Test.

The question is, why? Does the C++ standard clearly define whether these lines /*1*/ and /*2*/ should compile at all? If it does, which of the compilers is right from the standard's point of view? Can this inconsistent behavior be called a GCC bug, or do Clang and MSVC overlook some checks that they are supposed to perform?

I can understand that GCC might require a move constructor in order to create a temporary Test object from the inner braces ({1, 2}), and then move that object into an array. Hence the compilation error. But if that is so, why does it not fail on the line /*(2)*/ for the very same reason? This is the most confusing thing to me in this example.


By the way, here's an interesting observation: if I replace the definition of test_ with std::array<Test, 2> (instead of a "C-style" array), and replace the code in the constructor's initialization list with test_{{{1, 2}, {3, 4}}}, everything starts to compile successfully on all three mentioned compilers.

It is also unclear to me why does GCC not fail in this case on any line, while failing with a "raw" array.

Can anyone explain this as well?

like image 242
Taras Avatar asked Aug 19 '19 15:08

Taras


1 Answers

I see no problem with the initialization, so I think this is a GCC bug.


The initialization involved is list-initialization, so we consult [dcl.init.list]/3:

List-initialization of an object or reference of type T is defined as follows:

  • [...]

  • (3.3) Otherwise, if T is an aggregate, aggregate initialization is performed.

  • [...]

(An array is an aggregate.) Now we go to [dcl.init.aggr]/3:

When an aggregate is initialized by an initializer list as specified in [dcl.init.list], the elements of the initializer list are taken as initializers for the elements of the aggregate, in order. Each element is copy-initialized from the corresponding initializer-clause. If the initializer-clause is an expression and a narrowing conversion is required to convert the expression, the program is ill-formed.

So, for either of the two elements, we are effectively doing Test a = {1, 2}, which is valid because Test(int, int) is not explicit. Therefore, the initialization is well-formed and should be accepted by the compiler.

like image 132
L. F. Avatar answered Oct 23 '22 19:10

L. F.