1
votes

I have a class hierarchy with a diamond structure and a base class without default constructor, since it has reference members.

The code looks as follows:

class Base
{
public:
    Base( CustomType& obj ) : refObj_( obj ) {}
    virtual ~Base() {}
private:
    CustomType& refObj_;
};

class Left : public virtual Base
{
public:
    Left() {}; // ERROR: Compiler requests calling Base's constructor
    virtual void leftsMethod() = 0;
};

class Right : public virtual Base
{
public:
    Right( CustomType& obj ) : Base( obj ) {}; // Compiles, but Base( obj ) never gets called here
    virtual void rightsMethod() = 0;
};

class Bottom : public Left, public Right
{
public:
    Bottom( CustomType& obj ) : Base( obj ), Left(), Right( obj ) {}
};

Edit: Added virtual base destructor (original code has it)

Note that Left and Right are pure virtual classes, i.e. under no circumstances their constructors would call Base's constructor. This is due to virtual inheritance, implying that the most derived class Bottom calls Base's constructor.

My question: Why do I nevertheless have to call Base's constructor in the initialization list of Left? I'd like to avoid this and to pass the reference directly from Bottom's constructor only to Base. (I'm using MSVC 2010s "half C++11" compiler.)

Is there another solution for this combination of virtual inheritance and references in the base class?

3
Consistency. Humans would not expect adding or removing a =0 to the declaration of an unrelated member function (i.e. toggling if that member function is pure virtual or not) to change how the compiler treats Lefts constructor (e.g. compiles the constructor definition if the function is pure virtual, but does not compile if the function is virtual but not pure). Although, AFAIK, it's not a formal requirement of the standardisation process, few developers - including those on the standardisation committee - like the idea of a change of code causing something unrelated to break.Peter
@Peter Thanks! That's the answer I was looking for.staerkHelmis
Due to that, better to have virtual base with only default constructor. Add interface (pure virtual method) instead of (non defaulted) members.Jarod42
@Peter: As human, I found it more confusing to have initialization not called. That pure abstract class should not have to "call" virtual base class would be fine IMO.Jarod42
Actually, this code compiles just fine, starting with clang 3.4.1, gcc 7.1, and all available (on godbolt.org) msvc versions. The issue OP describes occurs only in lower versions. Anyone knows what has changed? @staerkHelmis maybe just switch to a newer version of your compiler?sebrockm

3 Answers

3
votes

You don't have a default constructor for Base, but Left still needs to initialize one when you create a Left object (one that is not a Bottom one).

The issue is that you have a reference in Base which means that you don't have a simple constructor possible.

I would say that you have a big design problem, as obj is initialized by Right, so the latter should hold obj, not Base.

Also where is the virtual destructor for Base?

class Base
{
public:
    Base() = default;
    ~Base() {}
};

class Left : public virtual Base
{
public:
    virtual void leftsMethod();
};

class Right : public virtual Base
{
    int& refObj_;
public:
    Right( int& obj ) : refObj_( obj ) {}
    virtual void rightsMethod();
};

class Bottom : public Left, public Right
{
public:
    Bottom( int& obj ) : Right( obj ) {}
};
2
votes

Design issues (which clearly exist here) aside, I fully agree with your reasoning that, because Left is an abstract class, no Left constructor will ever have to call any Base constructor and thus it is odd that it is required.

In fact, I tested your code with several compiler versions and it does compile just fine with gcc 7.1 and onwards, with clang 3.4.1 and onwards, and with all available msvc versions.

So, my assumption is, that this was just a bug in earlier compiler versions. Can anyone confirm this?

Also note, if you change virtual void leftsMethod() = 0; to virtual void leftsMethod() {}, so that Left is not abstract anymore, the error returns, even with the latest versions. And this makes perfectly sense, as now you could instantiate a Left instance and thus it would be up to Left's constructor to call one of Base's constructors.

Possible workaround

If you cannot switch to a newer compiler and if you cannot alter the implementation of Base, this may be a working solution for you:

Define a dummy instance of CustomType somewhere. Then, you can provide the "required" default constructors like this:

class CustomType{};

class Base
{
public:
    Base( CustomType& obj ) : refObj_( obj ) {}
private:
    CustomType& refObj_;
};

static CustomType dummy;

class Left : public virtual Base
{
protected:
    Left() : Base(dummy) {}; // note here
public:
    virtual void leftsMethod() = 0;
};

class Right : public virtual Base
{
protected:
    Right() : Base(dummy) {}; // and here
public:
    virtual void rightsMethod() = 0;
};

class Bottom : public Left, public Right
{
public:
    Bottom( CustomType& obj ) : Base( obj ), Left(), Right() {}
    virtual void leftsMethod() override {}
    virtual void rightsMethod() override {}
};

void test()
{
    CustomType c;
    Bottom b(c);
}

This is just to make the compiler happy, your argument that these constructors will never call Base(dummy) still holds.

Note however, that Base should really have a virtual destructor! I don't know if you just didn't include it here for brevity or if it really doesn't have one. If it doesn't, it's a very bad idea to build a class hierarchy on top of it.

0
votes

A quote from the official docs may describe the situation:

Practically speaking, this means that when you create a concrete class that has a virtual base class, you must be prepared to pass whatever parameters are required to call the virtual base class’s constructor. And, of course, if there are several virtual base classes anywhere in your classes ancestry, you must be prepared to call all their constructors. This might mean that the most-derived class’s constructor needs more parameters than you might otherwise think.

See this FAQ.