Different behavior of c++11 list-initialization
Asked Answered
V

1

13

Please consider the following code:

class A {
private:
  std::string s;
public:
  A() = delete;
  A(const A&) = delete;
  A(A&&) = delete;
  A(const std::string &a) : s(a) {}
};

Now, I would like to initialize an array of A using list initialization. g++ (4.9.1) could successfully build the following code:

int main() {
  A arr[2] = {{"a"}, {"b"}};
  return 0;
}

But, it failed for the following code:

class Aggr {
private:
  A arr[2];
public:
  Aggr() : arr{{"a"}, {"b"}} {}
};

The error messages are,

test.cc: In constructor ‘Aggr::Aggr()’:
test.cc:22:28: error: use of deleted function ‘A::A(A&&)’
   Aggr() : arr{{"a"}, {"b"}} {}
                            ^          
test.cc:11:3: note: declared here
   A(A&&) = delete;
   ^

That said, a list-initializer tries to call a move constructor for initializing an array inside of a class. That code, however, was successfully built by clang v3.5 without any warnings. So, I would like to know what the C++11 (or later version) specifies rules with respect to list-initialization. Thanks in advance.

Vulcanite answered 28/1, 2015 at 5:3 Comment(6)
@ZivS an rvalue referenceLanthanide
possibly relevant to your issue: #26686051Nally
@marcinj, yes, it's the same. Looks like a GCC bug gcc.gnu.org/bugzilla/show_bug.cgi?id=63707Bicolor
@JonathanWakely it looks very similar. But there are differences, as there is no user defined destructor involved, and the problem depends on A data members, but also on the way A move ctor are defined. See testcases in gcc.gnu.org/bugzilla/show_bug.cgi?id=64887Bonfire
@Christophe, what you've missed is that the std::string member here makes the destructor non-trivial, just like the user-defined destructor does in the other example. It's the same bug, I've closed it as a duplicate.Bicolor
@JonathanWakely I've seen your edit in the bug and I agree. The strange thing to mention are the two last testcases, because thebehaviour is different with an implicit move operator and a user defined one.Bonfire
B
2

Reading again and again the standard, I think this is a bug.

What does the standard say ?

8.5.1/2 : When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each member is copy-initialized from the corresponding initializer-clause.

It is explained that:

8.5/14 : (...) is called copy-initialization. [ Note: Copy-initialization may invoke a move (12.8). —end note ]

But I found no evidence in 12.8 that in your specific case a move would be required.

8.5.4/3 Otherwise, if T is a class type, constructors are considered. If T has an initializer-list constructor, the argument list consists of the initializer list as a single argument; otherwise, the argument list consists of the elements of the initializer list. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3).

So in principle you code should work !

Is it a bug ? Trying the experimental way

I commented out the delete of the move constructor, to benefit from the implicit move constructor. Strangely, I then got the following error message:

    Compilation error   time: 0 memory: 3232 signal:0

prog.cpp: In constructor 'Aggr::Aggr()':
prog.cpp:19:28: error: use of deleted function 'A::A(const A&)'
   Aggr() : arr{{"a"}, {"b"}} {}
                            ^
prog.cpp:10:3: note: declared here
   A(const A&) = delete  

So now he complains about a missing copy constructor !

Even more strangely, I then provided my own move constructor instead of the implicit one : here it compiled the code successfully !!

Finally I provided both a copy and a move and added some tracing:

class A {
private:
  std::string s;
public:
  A() = delete;
  A(const A&)  { std::cout<<"copy\n";} //= delete;
  A(A&&) { std::cout<<"move\n";} //= delete;
  A(const std::string &a) : s(a) {  std::cout<<"string ctor\n";}
};

And when I create an Aggr object, it just displays:

string ctor 
string ctor

showing that the array member is initialized form the string constructor using copy elision as we would have expected.

All these tests were performed with gcc-9.4.2 on ideone with C++14 option.

Conclusion

The fact that the same code fails to compile with implicit move ctor and succeeds with a user-defined move ctor looks very seriously like a bug.

The fact that the move constructor is not used when it's available reinforces this impression.

Consequently, I've reported this bug.

Bonfire answered 31/1, 2015 at 15:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.