iFreilicht
iFreilicht

Reputation: 14474

Bringing non-virtual interfaces and multi-level inheritance together

The Non-virtual Interface idiome (NVI) is pretty self explanatory: You don't write public virtual functions, but public functions that call a private virtual implementation function, like so:

class Object{
    virtual void v_load();
public:
    void load(){ v_load(); }
}

This enables you, the base class author, to check and enforce pre- and post-conditions or apply other functions so the author of deriving classes can't forget about them.

Now when you are the deriving author, you may want to write a base class yourself - let's call it Pawn - that extends on the functionality of load() and therefore has to override v_load(). But now you are facing a problem:

When you override v_load(), other clients that want to derive from your class, will always overwrite that behaviour, and they can not call Pawn::v_load() because it is a private function, neither can they call Pawn::load() because it is defined as { v_load; } in Object which will of course lead to an infinite loop. Additionally, requiring them to do so could lead to mistakes when they forget that call. If I would want them to enable that, I would have to specify the acces to v_load() as protected in Object, which seems like an ugly solution as it would weaken the encapsulation of Object greatly.

You could of course still override v_load() to call a new function v_pawnLoad(), which is then overridden by clients, but that seems very error-prone as a lot of clients will probably overload the wrong function.

So, how can I design Pawn in such a way that clients can still override v_load() while keeping the ability to check pre-conditions or call other functions and (if possible) not enabling, let alone requiring clients of Object or Pawn to call the base v_load() implementation?

Upvotes: 2

Views: 171

Answers (2)

Drax
Drax

Reputation: 13278

  • If your intention is to allow people to "extend" as opposed to "replace" load's behaviour, then put the code you currently have in v_load in load then call an empty v_load in the end.
  • Or you could just make v_load protected if you want to let people choose between "replacing" or "extending".
  • If you just want to allow them to replace the behaviour, your code is fine as it is.

As a bonus, in all these 3 variants you can change "allow" with "force" by making your v_load a pure virtual if you have no default behaviour.

If you wish to limit the override to your Pawn child class, add the final keyword to v_load in Pawn and use another virtual function to allow children of Pawn to customise its behaviour.

Upvotes: 2

Tom Knapen
Tom Knapen

Reputation: 2277

How about mixin' in some CRTP?

#include <iostream>

class BaseObject
{
private:
  virtual void v_load() = 0;

public:
  void load() { v_load(); }
};

template<typename Derived>
class Object : public BaseObject
{
private:
  virtual void v_load() { static_cast<Derived&>(*this).load(); }
};

class Pawn : public Object<Pawn>
{
public:
  void load() { std::cout << "Pawn::load()" << std::endl; }
};

class BlackPawn : public Pawn
{
private:
  virtual void v_load() {
    std::cout << "BlackPawn::v_load()" << std::endl;
    std::cout << "- "; Pawn::load();
  }

public:
  void load() {
    std::cout << "BlackPawn::load()" << std::endl;
    std::cout << "- "; Pawn::load();
  }
};

class BigBlackPawn : public BlackPawn
{
private:
  virtual void v_load() {
    std::cout << "BigBlackPawn::v_load()" << std::endl;
    std::cout << "- "; BlackPawn::load();
  }

public:
  void load() {
    std::cout << "BigBlackPawn::load()" << std::endl;
    std::cout << "- "; BlackPawn::load();
  }
};

template<typename T>
void load(T& x)
{
  x.load();
}


void vload(BaseObject& x)
{
  x.load();
}

int main()
{
  Pawn p;
  BlackPawn bp;
  BigBlackPawn bbp;

  load(p);
  load(bp);
  load(bbp);
  std::cout << std::endl;
  vload(p);
  vload(bp);
  vload(bbp);
}

Output on ideone.

Upvotes: 0

Related Questions