Summary: The case below involves using multiple inheritance for inheriting both an extended interface, and implementation for a basic interface.
I used virtual multiple inheritance to put in place the following system:
I have a hierarchy of abstract interfaces:
ICommon
/ \
ISpecific1 ISpecific2
As expected, the specific interfaces add functionality over the common functionality derived from ICommon.
I also have classes implementing the interfaces:
The application eventually uses only SpecificImp1 and SpecificImp2. But CommonImp is required in order to avoid implementing ICommon twice (in SpecificImp1, SpecificImp2).
This means that, e.g., SpecificImp1 needs to inherit ISpecific1 for the whole interface it needs to expose, and CommonImp for the implementation of the common part. But both ISpecific1 and CommonImp inherit ICommon (one in order to extend the interface, and one for implementation). Which makes SpecificImp1 inherit indirectly twice from ICommon. Using virtual inheritance handled the diamond inheritance issue.
This is the minimal reproducible code example:
#include <iostream>
struct ICommon
{
virtual void DoCommon() = 0;
};
struct ISpecific1 : public virtual ICommon
{
virtual void DoSpecific1() = 0;
};
struct ISpecific2 : public virtual ICommon
{
virtual void DoSpecific2() = 0;
};
struct CommonImp : public virtual ICommon
{
virtual void DoCommon() override { std::cout << "CommonImp::DoCommon" << std::endl; }
};
struct SpecificImp1 : public ISpecific1, public CommonImp
{
virtual void DoSpecific1() override { std::cout << "SpecificImp1::DoSpecific1" << std::endl; }
};
struct SpecificImp2 : public ISpecific2, public CommonImp
{
virtual void DoSpecific2() override { std::cout << "SpecificImp2::DoSpecific2" << std::endl; }
};
int main()
{
SpecificImp1 s1;
s1.DoCommon();
s1.DoSpecific1();
SpecificImp2 s2;
s2.DoCommon();
s2.DoSpecific2();
return 0;
}
This design serves me well so far, but it's quite cumbersome. And multiple inheritance is always something you should consider alternatives for.
So, my question is, given this system, can you suggest a good alternative?
BTW - All types above are structs with everything public. This was done to make the code shorter, please ignore it.
As modification of point 1 in your answer, you do not have to make CommonImp
a member nor duplicate anything:
#include <iostream>
#include <vector>
struct ICommon
{
virtual void DoCommon() = 0;
};
struct ISpecific1
{
virtual void DoSpecific1() = 0;
};
struct ISpecific2
{
virtual void DoSpecific2() = 0;
};
struct CommonImp : public ICommon
{
virtual void DoCommon() override { std::cout << "CommonImp::DoCommon" << std::endl; }
};
struct SpecificImp1 : public ISpecific1, public CommonImp
{
virtual void DoSpecific1() override { std::cout << "SpecificImp1::DoSpecific1" << std::endl; }
};
struct SpecificImp2 : public ISpecific2, public CommonImp
{
virtual void DoSpecific2() override { std::cout << "SpecificImp2::DoSpecific2" << std::endl; }
};
int main()
{
SpecificImp1 si1;
SpecificImp2 si2;
si1.DoCommon(); // 1.
si2.DoCommon(); // 2.
{ // begin new scope to make sure the addresses stay valid
std::vector<ICommon*> imps;
imps.push_back(&si1); // 3.
imps.push_back(&si2); // 4.
}
return 0;
}
This solves the general requirement. No virtual inheritance for the specific interfaces needed, no diamond inheritance.
However, in the code above, it is not enforced by the specific interfaces that the specific implementations also have to provide the ICommon
interface. But it is indirectly enforced by various usages: By calling DoCommon()
in 1. and 2. and by converting the address ob the specific objects to ICommon*
in 3. and 4. So the simplest solution would be to leave it at that.
We can (to a degree) enforce the inheritance of ICommon
even within the specific interfaces by demanding a member function getICommonP
, which returns an ICommon
pointer (see code below). The specific implementations inheriting from ICommon
would just return this
. The compiler would throw an error, if this abstract member function is not implemented. This function has the added benefit of making it possible to convert from a specific interface to the common interface (5. and 6.). The specific implementation can be directly converted (3. and 4.), because it inherits from ICommon
. See lines marked with '// added'.
#include <iostream>
#include <vector>
struct ICommon
{
virtual void DoCommon() = 0;
};
struct ISpecific1
{
virtual ICommon* getICommonP() = 0; // added
virtual void DoSpecific1() = 0;
};
struct ISpecific2
{
virtual ICommon* getICommonP() = 0; // added
virtual void DoSpecific2() = 0;
};
struct CommonImp : public ICommon
{
virtual void DoCommon() override { std::cout << "CommonImp::DoCommon" << std::endl; }
};
struct SpecificImp1 : public ISpecific1, public CommonImp
{
virtual ICommon* getICommonP() override { return this; } // added
virtual void DoSpecific1() override { std::cout << "SpecificImp1::DoSpecific1" << std::endl; }
};
struct SpecificImp2 : public ISpecific2, public CommonImp
{
virtual ICommon* getICommonP() override { return this; } // added
virtual void DoSpecific2() override { std::cout << "SpecificImp2::DoSpecific2" << std::endl; }
};
int main()
{
SpecificImp1 si1;
SpecificImp2 si2;
{
std::vector<ICommon*> imps;
imps.push_back(&si1); // 3.
imps.push_back(&si2); // 4.
ISpecific1* si1p = &si1;
ISpecific2* si2p = &si2;
imps.push_back(si1->getICommonP()); // added 5.
imps.push_back(si2->getICommonP()); // added 6.
}
return 0;
}
A more natural member function would be the conversion operator virtual operator ICommon&()
within the interfaces instead of virtual ICommon* getICommonP()
, but some compilers deliver a warning, when your implementations also inherit from the same class, because the conversion to references to base classes is done automatically in this case without calling the explicit conversion operator member function.
On a side note: The specific interfaces are compatible with the solution in point 1 in your answer (except the common interface forwarding): Instead of a pointer to the class itself ('this') the getICommonP()
functions would return a pointer to the ICommonImp
member. The implementation could decide, how to implement and provide the common interface - inheriting from it or keeping the common implementation as member variable.
If you do not like an added member function, an alternative for enforcing inheritance of the specific implementations from the interface ICommon
within the specific interfaces would be to make the specific interfaces into templates and inheriting from them with:
struct SpecificImp2 : public ISpecific2<SpecificImp2>, public CommonImp
Then template<class T> Ispecific2<T>
could test (or require), whether T
inherits from ICommon
and from ISpecific2<T>
.
This would go into the direction of point 3 in your answer.
As you now have not one Ispecific2
, but many ISpecific2<>
(because of it being a template) - and you often need one interface, you could have all the templated classes ISpecific2Templ<>
inherit from the same actual specific interface ISpecific2
.