samedi 25 novembre 2017

Reducing boilerplate for base to derived delegation in non-virtual polymorphic classes

Consider a closed1 class hierarchy such as the following:

class B {...}; 
class D1 final : public B {...};
class D2 final : public B {...};

Where B is a abstract2 base class and D1 and D2 are its derived classes.

Due to implementation constraints or design, none of these classes have any virtual methods but methods on B that have different implementations in D1 and D2 are simply delegated by making a runtime check of the derived type, as follows:

class B {
  bool isD1;
protected:
  B(bool isD1) : isD1{isD1} {}
public:
  std::string to_string() {
    return isD1 ? static_cast<D1*>(this)->to_string() : static_cast<D2*>(this)->to_string();
  }
}

class D1 final : public B {
public:
  D1() : B(true) {}
  std::string to_string() { // D1 specific implementation ... }
}

class D2 final : public B {
public:
  D2() : B(false) {}
  std::string to_string() { // D2 specific implementation ... }
}

Here the to_string method on B simply checks if the most-derived type of B is D1 or D2 and calls the appropriate method (also called to_string in both cases).

Cool.

Now imagine there are 10 more methods like B::to_string. What can I do in C++11 to reduce the delegation boilerplate in B, without resorting to macros?

In C++14 it seems a reasonable approach would be a generic delegation mechanism, like:

class B {
  ...

  template <typename F>
  auto delegate(F&& f) -> decltype(f(D1{})) {
    return isD1 : f(*static_cast<D1*>(this)) : f(*static_cast<D2*>(this));
  }

  std::string to_string() {
    return delegate([](auto&& b){ return b.to_string(); });
  }
}

Here the [](auto&& b){ return b.to_string(); } generic lambda works whether ultimately passed a D1 or D2 (since both have to_string methods). In C++11 I don't see an equivalently concise way to express this.

Any ideas?

Of course, you could use macros to duplicate a non-generic macro and pass it to a 2-argument delegate method (that takes separate functors for D1 and D2) but I'd like to avoid macros.


1 Here closed means that the set of derived classes of B is fixed and known at runtime.

2 Abstract in concept but not in the "pure virtual" sense. That is this class should not be directly instantiated - the only entire objects that make sense are its derived classes. The various constructors are made protected to enforce this.

Aucun commentaire:

Enregistrer un commentaire