Question

Initialization order of inherited constructors

I have this example:

#include <iostream>
#define print(X) std::cout << X << std::endl

struct B1 {
    B1(int _i = 5): i(_i) { print("B1 constructor"); };
    int i;
};

struct B2 {
    B2(int _j = 7): j(_j) { print("B2 constructor"); }
    int j;
};

struct D : B2, B1 {
    using B1::B1;
};

int main(void)
{
    D d = D{10};
    print("B1::i = " << d.i);
    print("B2::j = " << d.j);
}

The output of this program is:

B2 constructor
B1 constructor
B1::i = 10
B2::j = 7

Per §11.9.4[class.inhctor.init]/1:

When a constructor for type B is invoked to initialize an object of a different type D (that is, when the constructor was inherited ([namespace.udecl])), initialization proceeds as if a defaulted default constructor were used to initialize the D object and each base class subobject from which the constructor was inherited, except that the B subobject is initialized by the inherited constructor if the base class subobject were to be initialized as part of the D object ([class.base.init]). The invocation of the inherited constructor, including the evaluation of any arguments, is omitted if the B subobject is not to be initialized as part of the D object. The complete initialization is considered to be a single function call; in particular, unless omitted, the initialization of the inherited constructor's parameters is sequenced before the initialization of any part of the D object.

Firstly, per §11.9.4/1, since D inherits constructor B1(int) from base B1, that inherited constructor can initialize subobject B1; further, the parameter _i is fully initialized before initializing any part of D, hence the inherited constructor D::B1(int) is selected by overload resolution which initializes B1::i members by mem-initializer-list.

Since B2 constructor is printed first, this means that B2 constructor is called before B1. So how member B2::j is initialized with default argument 7 not value 10 that's passed in the call D{10}?

I'm not sure whether that happened, but I can't understand the sequence of execution in this case. and where this behavior is necessary in the standard.

 5  177  5
1 Jan 1970

Solution

 6

but I can't understand the sequence of execution

The important thing here is that the "only" the inherited ctor(B1::B1() here) will use the D's ctor's parameter to initialize its data member.

This here means that since you've inherited B1's ctor, the parameter _i in the ctor D::D(int _i) will be passed as an argument to initialize i of the B1 subobject.

Baiscally, your code class' D is equivalent to writing:

struct D : public B2, public B1
{
  inline D(int _i) 
  : B2(7)
//--vvvvvv------------->this is because you inherited B1's ctor
  , B1(_i)
  {
  }
  
};

This is given in your quoted class.inhctor.init only which has been highlighted:

When a constructor for type B is invoked to initialize an object of a different type D (that is, when the constructor was inherited ([namespace.udecl])), initialization proceeds as if a defaulted default constructor were used to initialize the D object and each base class subobject from which the constructor was inherited, except that the B subobject is initialized by the inherited constructor if the base class subobject were to be initialized as part of the D object ([class.base.init]). The invocation of the inherited constructor, including the evaluation of any arguments, is omitted if the B subobject is not to be initialized as part of the D object.The complete initialization is considered to be a single function call; in particular, unless omitted, the initialization of the inherited constructor's parameters is sequenced before the initialization of any part of the D object.

(emphasis mine)


You can additionally verify this by inheriting B2's ctor by doing using B2::B2. Then your class D will be equivalent to:

struct D : public B2, public B1
{
  inline D(int _j)
//--vvvvvv------------>this happens when you inherit B2's ctor
  : B2(_j)
  , B1(5)
  {
  }
  
};
2024-07-10
user12002570

Solution

 0

This is expected. Note that to construct D you have to call constructors for all ancestors, so this means B1 and B2. Order of invoking those constructors is enforced by order of inheritance list. Not this is old C++ rule which has to be maintained forever.

So when D is constructed default constructor for B2 is called then constructor for B1 is called with argument you have passed to it.

Better demo:

#include <print>
#include <source_location>

void log(int val, const std::source_location& from, const std::source_location& loc = std::source_location::current())
{
    std::println("{}:{} val={} invoked from: {}:{}", loc.function_name(), loc.line(), val, from.function_name(), from.line());
}

struct B1 {
    B1(int _i = 5, const std::source_location& loc = std::source_location::current())
        : i(_i)
    {
        log(i, loc);
    };
    int i;
};

struct B2 {
    B2(int _j = 7, const std::source_location& loc = std::source_location::current())
        : j(_j)
    {
        log(j, loc);
    }
    int j;
};

struct D : B2, B1 {
    using B1::B1;
};

int main(void)
{
    D d = D { 10 };
    std::println("B1::i = {}", d.i);
    std::println("B2::j = {}", d.j);
}

Log output:

B2::B2(int, const std::source_location &):22 val=7 invoked from: D::B1(int, const std::source_location &):28
B1::B1(int, const std::source_location &):13 val=10 invoked from: int main():33
B1::i = 10
B2::j = 7

Note that compiler created constructor which is called: D::B1!

2024-07-10
Marek R