2

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?

101010
  • 41,839
  • 11
  • 94
  • 168
iFreilicht
  • 13,271
  • 9
  • 43
  • 74
  • _specify the acces to v_load() as protected in Object, [...] seems like an ugly solution as it would weaken the encapsulation of Object greatly_ - Care to elaborate? `v_load` is essentially already public, as it is trivially encapsulated by the public `load`. Can you give a specific example where calling `v_load` directly from a subclass would be harmful? If so, how does that match with your initial question, which tries to achieve just that, calling `v_load` directly from a subclass. – ComicSansMS Aug 21 '14 at 14:59
  • `specify the acces to v_load() as protected ... it would weaken the encapsulation of Object greatly`. What sort of arguments do you have for your opinion? It loosens encapsulation just enough to allow exactly what you want: invoke the version in parent. – eerorika Aug 21 '14 at 14:59
  • 1
    @ComicSansMS `load` can control the conditions under which `v_load` is called. Nobody can call `v_load` without invoking `load`. By specifying `v_load` as `protected`, anybody who inherits from `Object` can call `v_load` freely, which gives the author of `Object` less control about how it is used. – iFreilicht Aug 21 '14 at 15:14
  • @user2079303 but that's *not* what I really want. I want `Pawn` to always execute its own boilerplate for `v_load` before and after `v_load` of inheriting classes is called. Invoking the version of `Pawn` in inheriting classes is only a workaround to achieve that behaviour, but it ultimately gives `Pawn` less control about `v_load` as a client could just decide to not call the base implementation or simply forget to. – iFreilicht Aug 21 '14 at 15:16

2 Answers2

2
  • 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.

Drax
  • 12,682
  • 7
  • 45
  • 85
  • The problem is, clients of `Object` should be able to extend the behaviour, which is what the code does. Clients of `Pawn` should be able to extend the behaviour of `Pawn`, not `Object`. That's what I'm struggling with. – iFreilicht Aug 21 '14 at 15:10
  • @iFreilicht make the override `final` in `Pawn` and use another virtual function to allow `Pawn` clients to extend `Pawn`. – Drax Aug 21 '14 at 15:19
  • Not perfect, but definitely the best solution so far. I didn't know you could do that with virtual functions, thank you! – iFreilicht Aug 21 '14 at 15:25
  • @iFreilicht its c++11 :) – Drax Aug 21 '14 at 15:27
  • Could your probably add the `final override` to your answer? If so, I would be happy to upvote and accept it. – iFreilicht Aug 21 '14 at 15:35
0

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.

Tom Knapen
  • 2,277
  • 16
  • 31
  • Very cool idea, but by overriding the non-virtual function `load`, this does not behave correctly when actually trying to use the polymorphic behaviour. If I use an `Pawn*` that points to a `BigBlackPawn`, a call to `load` will not call `BigBlackPawn::load`. [forked your ideone](http://ideone.com/t1AVjp) – iFreilicht Aug 21 '14 at 15:21
  • @iFreilicht That's because `T` will be deduced to be `Pawn` in `load(*p)`. That function is there just to show that `vload()` generates the same output. Since you are probably going to be storing `BaseObjects` in a collection of some sort (and thus be losing the real types), you're only going to be using the `vload(BaseObject&)`, which will do what you requested. See [this](http://ideone.com/zOfRBM) ideone – Tom Knapen Aug 21 '14 at 15:45
  • Ok, it works, I'll give you that, in your solution you saved my loop problem, which didn't really require CRTP, unless I'm mistaken. See [this ideone](http://ideone.com/18KkbZ). So kudos for that, but to be honest, I like Drax' solution better, it doesn't give the clients a chance to forget the calling of `load` of their parent and neither requires them to redefine `load` in their own class definition. – iFreilicht Aug 21 '14 at 16:08