Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different behavior of c++11 list-initialization

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.

like image 674
Choonki Jang Avatar asked Jan 28 '15 05:01

Choonki Jang


1 Answers

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.

like image 159
Christophe Avatar answered Nov 05 '22 16:11

Christophe