5

I have a class Base which has a parameterized constructor and two classes Middle1 and Middle2 which virtually inherit from Base (in order to solve the diamond problem). In addition, class Foo inherits from Middle1 and Middle2.

Foo now calls the Base constructor explicitly and passes the parameter.

class Base { private: int value; protected: Base(int& value) { this->value = value; } }; class Middle1 : virtual public Base { protected: Middle1() { } }; class Middle2 : virtual public Base { protected: Middle2() { } }; class Foo : public Middle1, public Middle2 { public: Foo(int& value) : Base(value) { } }; 

However, the code does not compile because Base lacks a default constructor and thus Middle1 and Middle2 don't have a default constructor to call, but need one which can be called by Foo.

Of course, I could now change the constructors of Middle1 and Middle2 to call the constructor of Base, too:

Middle1(int& value) : Base(value) { } //... Middle2(int& value) : Base(value) { } // and then change my constructor of Foo: Foo(int& value) : Base(value), Middle1(value), Middle2(value) { } 

However, it seems rather clunky - especially given that because of the virtual inheritance, the Base constructor will never be called by Middle1 or Middle2 (they are not meant to be initialized, i.e. are basically abstract classes). Thus, having to introduce the parameter to the constructors of both Middle1 and Middle2 seems rather useless.

Is there any other way to make the above code compile without also having to introduce parameterized constructors in Middle1 and Middle2?

1
  • The problem is related to construction of Middle1 and Middle2 - since you have implemented their default constructor - and is not caused by Foo. The compiling fails because your definitions of the constructors for Middle1 and Middle2 both implicitly call the the default constructor of Base (e.g. Middle1() : {} is equivalent to Middle1(): Base() {} which calls the default constructor). Deriving Foo from Middle1 and Middle2 doesn't change that - in fact, if you remove the definition of Foo the code will still not compile. Commented Sep 30, 2021 at 5:39

2 Answers 2

4

In virtual inheritance, the most-derived class has to directly call all of its ancestor constructors. Since Base doesn't have a default constructor, Middle1() and Middle2() can't compile if they can't pass an int& to Base().

However, in the code you have shown, there is no reason for Base() to take an int by reference. Pass it by value instead, and then Middle1() and Middle2() can pass 0 to Base():

class Base { private: int value; protected: Base(int value = 0) { this->value = value; } }; class Middle1 : virtual public Base { protected: Middle1() { } }; class Middle2 : virtual public Base { protected: Middle2() { } }; class Foo : public Middle1, public Middle2 { public: Foo(int value) : Base(value) { } }; 

Though, I would suggest passing a pointer (or a std::optional) instead:

class Base { private: int value; protected: Base(int* avalue = nullptr) { if (avalue) this->value = *avalue; } }; class Middle1 : virtual public Base { protected: Middle1() { } }; class Middle2 : virtual public Base { protected: Middle2() { } }; class Foo : public Middle1, public Middle2 { public: Foo(int& value) : Base(&value) { } }; 
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! The int& is just for illustration - in reality it's a struct reference. But your comment just made me realize that I could just let Middle1 and Middle2 pass an empty value of that given struct. Maybe still not perfect, but already much better. Or based on the example: static int DUMMY_VALUE = 0; /*...*/ Middle1() : Base(DUMMY_VALUE). Or just change the reference to a pointer.
1

You ran into an annoying omission in the C++ standard: There is support for pure virtual functions, i.e. functions, that will only be declared in a further derived class. But there is no support for pure virtual base classes, i.e. base classes, that will always be initialized from a further derived class.

My solution is to provide a default constructor for Base, that references a declared, but undefined (!) static member:

class Base { int value; static const int Base_Default_Constructor_Used_Illegally; protected: Base(int& value) : value(value) { } Base() : value(Base_Default_Constructor_Used_Illegally) { } }; class Middle1 : virtual public Base { protected: Middle1() = default; }; class Middle2 : virtual public Base { protected: Middle2() = default; }; class Foo : public Middle1, public Middle2 { public: Foo(int& value) : Base(value) { } }; 

So what will happen? This module compiles fine, because Middle1 and Middle2 can both default-construct Base, as we gave it a default constructor. However, Base's default constructor will never be emitted into the object file produced by the compiler, because it is never called: When constructing Foo implicitly calls the constructor of Middle1 and Middle2, it will only use the shortened version of that constructor, that does NOT initialize the virtual base class(es). So this code not only compiles fine, it also goes well through the linker stage without issues.

However, should you accidentally instantiate a sole object of Middle1 or Middle2 or a default-constructed Base somewhere, it will actually emit code for the default constructor of Base and then you will get a linker error about an undefined symbol Base_Default_Constructor_Used_Illegally. The error message will tell you, in which module you triggered this unwanted instantiation, but it won't tell you the precise line number. So the error message is not perfect, but in most cases, it will probably be enough to find the source of the error. If not, try to replace the default constructor of Base with one, that throws an exception, and then run your code until that exception is thrown and use a stack trace to see, where the wrong default construction was triggered.

The above trick has been tested with both GCC and CLANG, but I am quite sure, that it will work with all major C++ compilers. Note though, that the trick of relying on the compiler optimizing out a variable access is definitely not C++ standards compliant. However, the trick is not using some "undefined behavior", as the initialization value(Base_Default_Constructor_Used_Illegally) is actually ill-formed, if the static member is not defined in some translation unit. So, the worst thing, that the compiler may do, is to reject this trick (for example, if it always ejects that default constructor into the object code, linking will always fail), but the compiler may not start doing something nasty instead, if it just chooses to ignore the undefined variable, because the default constructor is never used for real.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.