Search code examples
c++c++11gcc-warning

converting to ‘A’ from initializer list would use explicit constructor ‘A::A(int)’


I am trying to migrate an old C++03 codebase to C++11. But I fail to understand what gcc is warning me about in the following case:

% g++ -std=c++03 t.cxx
% g++ -std=c++11 t.cxx
t.cxx: In function ‘int main()’:
t.cxx:8:21: warning: converting to ‘A’ from initializer list would use explicit constructor ‘A::A(int)’
    8 | int main() { B b = {}; }
      |                     ^
t.cxx:8:21: note: in C++11 and above a default constructor can be explicit
struct A {
  explicit A(int i = 42) {}
};
struct B {
  A a;
};
    
int main() {
  B b = {};
  return 0;
}

All I am trying to do here is a basic zero initialization. It seems to be legal for C++03, but I fail to understand how to express the equivalent in C++11.


For reference, I am using:

% g++ --version
g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0

Solution

  • The given program is ill-formed for the reason(s) explained below.

    C++20

    B is an aggregate. Since you're not explicitly initializing a, dcl.init.aggr#5 applies:

    1. For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows:

    5.2 Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).

    This means that a is copy initialized from an empty initializer list. In other word, it is as if we're writing:

    A a = {}; // not valid see reason below
    

    Now we move onto dcl.init.list#3.5:

    Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.

    This means that the object will be value initialized.

    Now to value initialize:

    To value-initialize an object of type T means:

    • if T is a (possibly cv-qualified) class type ([class]), then
      • if T has either no default constructor ([class.default.ctor]) or a default constructor that is user-provided or deleted, then the object is default-initialized;

    So we move onto default initialize:

    If T is a (possibly cv-qualified) class type ([class]), constructors are considered.The applicable constructors are enumerated ([over.match.ctor]), and the best one for the initializer () is chosen through overload resolution ([over.match]). The constructor thus selected is called, with an empty argument list, to initialize the object.

    Finally from over.match.ctor:

    When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors ([class.conv.ctor]) of that class. The argument list is the expression-list or assignment-expression of the initializer.

    This means that only the converting ctor are candidates. And since A::A(int) is explicit, it is not a converting ctor and thus there the set of candidates is empty and the program(A a ={};) is ill-formed.


    Essentially, the reason for the failure is that A a = {}; is ill-formed.


    Solution

    To solve this, we can pass A{} or A{0} as the initializer inside the list as shown below:

    B b = { A{} };  //ok now 
    B c = { A{0} }; //also ok
    

    Working demo.


    Note

    Note that writing A a{}; on the other hand is well-formed as this is a direct-initialization context and so it is direct-list-initialization.