9

I originally asked this question on StackOverflow, but I was directed here, and I think my problem is perhaps as much conceptual as technical, so here goes.

If you’re defining a hierarchy of abstract class in C++, and then creating concrete subclasses with implementations, you could end up with abstract classes something like this, say:

  A
 / \
B1 B2

So the concrete classes then inherit like so:

B1   B2        B1   B2
 |    |         |    |
C1   C2        D1   D2

And this is all fine and dandy when Cn and Dn are just implementing the interfaces of Bn, or where say C1 and C2 implement the A interface differently.

However, if I want to have some shared functionality in C1 and C2, which comes from the A interface, where do I put it?

It can’t go in A, both because A is abstract and because Dn should not be inheriting it.

It seems like there’s a notional A_for_C implementation, but does this belong in another ancestor class? Or in a composed sibling class?

   _____A_____                       _____A_____
  /     |     \                     /     |     \
B1   A_for_C  B2        vs        B1     B2     A_for_C
 |_____/ \____ |                   |      |
C1            C2                  C1     C2

                               (C1 and C2 then each have an A_for_C and delegate)

The first one seems conceptually accurate, but requires virtual inheritance, while the second requires delegation. So both impose a performance hit despite there not being any real ambiguity.

Reading around the web, on this website I find it said

Some people believe that the purpose of inheritance is code reuse. In C++, this is wrong. Stated plainly, “inheritance is not for code reuse.”

How then should the implementation be shared?

Further thoughts

I found some relevant discussion in these questions:

I think utnapistim’s answer below is much pithier and to the point than these, and helped me mentally cut through a lot of what these other questions/answers discuss.

Inheritance is about agreeing to fulfil a contract. Multiple inheritance is fine if the subclass really does guarantee fulfilling the parent contracts.

Implementation, however, is only really the concern of the final object. Yes, it might be convenient to inherit the implementation sometimes, but that’s actually orthogonal to the interface, and there are various techniques for pulling in an implementation other than the default v-table-based approach, including:

(Which are, I think, equivalent except that one is compile-time and one is run-time.)

2 Answers 2

7

There are two aspects to your question:

§1. What is c++ inheritance for (if not for code reuse)?

The simplest answer to give here is "implementing a contract" (also see Liskov substitution principle and facade design pattern).

§2. How then should the implementation be shared?

Consider:

  • Encapsulation of an object, implementing common behavior to both branches (this is sometimes the best alternative to inheritance diamonds). Your A_for_C doesn't need to inherit from A.
  • Free functions (templated if your algorithm applies to multiple types).
  • Template classes (CRTP may be an alternative to a diamond inheritance pattern).
1
  • Yes I think you’ve hit the nail on the head with there being two elements to my question. I think what was stumping me was that I feel comfortable with the principles (if not the practice) behind §1, but I thought I would have to compromise them to deal with §2. CRTP is an inspired approach, and absolutely fits the concrete use-case I’ve got here: picking SDL vs a native runtime. The interfaces need to be polymorphic, but can be determined at compilation, so they don’t need runtime polymorphism. Thanks!
    – Leo
    Commented Jun 27, 2014 at 20:48
4

I think the most of the "inheritance" problems are in its name. Especially when dealing with C++, that as a multiparadigm language does not necessarily have to obey to the OOP paradigms, pattern and terminology.

If viewed aside from any paradigm-specific terminology, C++ offers two "composition" (with plain English memaning) methods:

  • explicit embedding ( struct A { B m; } a; a.m.fn(); // B::fn )
  • implicit embedding ( struct A: B {} a; a.fn(); // B::fn )

(I didn't use "composition" and "inheritance" just to don't "distract" into OOP terminology)

The second produce the same data structure layout as the first, it simply makes the m name implicit and makes A to implicitly behave like B. If we don't make virtual function to come into play, this is just a way to import the B defined behavior into A without the need to write more code in A itself.

There is no intention to apply any substitution principle, here. There is no OOP concept in this. This is not what OOP school call "inheritance". It just incidentally have the same name given by the OOP school and the C++ laguage spec.

std::true_type inherits from std::integal_constant. None of them has virtual methods (included the destructor). From a OOP purist this is a blasphemy, but nerveless, it's part of the C++ standard library.

When you make virtual function to come into play, then you acquire the capability to "override" a behavior with another (may be declared as undefined one). This makes C++ classes to become very similar to what OOP school calls "Object", and implicit embedding what they call "inheritance".

But C++ offer another most often ignored "substitution mechanism" (together with virtual functions): DOMINANCE. It is often ignored because is meaning less with single inheritance languages, but plays in multiple inheritance.

Consider this:

struct Inteface_A
{
    virtual fnA() = 0;
    virtual ~Inteface_A() {};
};

struct Implementation_A_comon
{
    virtual fnA() { cout << "common implementation of IA" << endl; }
    virtual ~Implementation_A_comon() {}
};

struct Implementation_A_special
{
    virtual fnA() { cout << "special implementation of IA" << endl; }
    virtual ~Implementation_A_special() {}
};



class Actual_object:
   public Inteface_A,
   protected Implementation_A_comon,
   public Interface_B, //not declared here, but may be in another header
   protected Implementation_B_common // not declared here, may be in yet another header
{
};

class Another_object:
   public Inteface_A,
   protected Implementation_A_special
{
};

Here both Actual_object and Another_object can be valid substitute of Inteface_A, and since fn is pure (abstract, in other literature) a call to ia->fn() makes the call to end-up in the only valid fn method inherited from the derived object. And it is the one provided by the inherited "implementation" inherited (in C++ pure sense) class. This is dominace.

You can easily imagine this extended with a number of different interface and a number of different "partial implementation", inheriting each-other in various way, completing each other providing different behaviors to be imported into a final object.

Is it orthodox OOP? Absolutely no. Is it "valid OOP"? respect to interface yes, respect to implementations no (or at least, not properly). Is this valid C++: yes. And also a good code reuse, (no implementation to rewrite many many times into every object that has a same interface to implement) using run-time-type polymorphism, staying out from templates and static polymorphism, not adequate where different object type have to coexist into a same runtime context: CRTPs IA<A> and IA<B> are both named IA, have the same interface, but from a runtime perspective are unrelated. You cannot have a std::vector<something> that can refer them both.

Something the pure OOP school simply don't accept just because ... they don trust their own programmers in understanding all this. But to me... that's their fault, not MI's.

2
  • I like this. So long as the naming is consistent I don’t think the deviation from orthodox OOP is going to be a huge issue. Public inheritance fulfils contracts, protected inheritance implements. CRTP is better if everything can be resolved at compile-time, but as you say it means you can’t create a heterogenous container of subclass instances.
    – Leo
    Commented Aug 18, 2014 at 8:34
  • 1
    This won't compile as is. You have to implement all abstract methods in Actual_object and Another_object and than delegate to the correct implementation. And this is the main drawback of this method...
    – Zelta
    Commented May 6, 2018 at 16:37

Not the answer you're looking for? Browse other questions tagged or ask your own question.