topsframework/tops

Evaluators

Closed this issue · 4 comments

@igorbonadio . As we described by e-mail, we are trying to find a way to create a class DecodableEvaluator, which will use one of SimpleEvaluatorImpl<Model> or CachedEvaluatorImpl<Model> and double dispatch the call to the Model classes.

Our problem is: we have a EvaluatorImpl interface to abstract templates and allow virtual methods in the main hierarchy. We cannot count on lazy code generation (compiler will not create template class methods if they are not used) because this mechanism is disabled for virtual methods. But to have this implementation hierarchy:

     .---------------.
     | EvaluatorImpl |
     '---------------'
             ^
             |
 .------------------------.
 | SimpleEvaluatorImpl<T> |
 '------------------------'
             ^
             |
 .------------------------.
 | CachedEvaluatorImpl<T> |
 '------------------------'

We will need the functions to be virtual. And I believe I found a solution to that. We need to use partial template specialization, as described in: http://stackoverflow.com/questions/24936862/c-template-specialization-for-subclasses-with-abstract-base-class

With one extra template parameter, we can check if the Model is a subclass of DecodableModel. In this case, we can use DecodableModel methods. Otherwise, we have default implementations which will not be used

It sounds good!

I tried to implement the solution I purposed, and after some time stucked in problems with method lookup, I got to solve it. Just to show (the entire implementation is still not ready), I made a "minimal example", with generic (although related) names. Take a look:

#include <memory>
#include <iostream>
#include <type_traits>

/* CLASS FooImpl **************************************************************/

class FooImpl : public std::enable_shared_from_this<FooImpl> {
 public:
  virtual void run() = 0;
};

using FooImplPtr = std::shared_ptr<FooImpl>;

/* CLASS SimpleFooImpl ********************************************************/

// Forward declarations
class Bar;

template<typename T, typename Bar = void>
class SimpleFooImpl
    : public FooImpl {
 public:
  virtual void run() override {
    std::cout << "Doing nothing... (not son of Bar)" << std::endl;
  }  
};

template<typename T>
class SimpleFooImpl<T, typename std::enable_if<std::is_base_of<Bar,T>::value>::type>
    : public FooImpl {
 public:
  virtual void run() override {
    T t{};
    t.simpleRun(
      std::static_pointer_cast<SimpleFooImpl<
        T, typename std::enable_if<std::is_base_of<Bar,T>::value>::type
      >>(this->shared_from_this())
    );
  }
};

template<typename T>
using SimpleFooImplPtr = std::shared_ptr<SimpleFooImpl<T>>;

/* CLASS CachedFooImpl ********************************************************/

template<typename T, typename Bar = void>
class CachedFooImpl
    : public SimpleFooImpl<T,Bar> {

 public:
  // Derived class getter
  int cache() {
    return _cache;
  }

 private:
  // Derived class attribute
  int _cache = 0;
};

template<typename T>
class CachedFooImpl<T, typename std::enable_if<std::is_base_of<Bar,T>::value>::type>
    : public SimpleFooImpl<T, typename std::enable_if<std::is_base_of<Bar,T>::value>::type> {
 public:
  virtual void run() override {
    T t{};
    t.cachedRun(
      std::static_pointer_cast<CachedFooImpl<
        T, typename std::enable_if<std::is_base_of<Bar,T>::value>::type
      >>(this->shared_from_this())
    );
  }

  // Derived class getter
  int cache() {
    return _cache;
  }

 private:
  // Derived class attribute
  int _cache = 0;
};

template<typename T>
using CachedFooImplPtr = std::shared_ptr<CachedFooImpl<T>>;

/* CLASS Bar ******************************************************************/

class Bar {
 public:
  virtual void run(std::string type) = 0;
};

/* CLASS BarDerived ***********************************************************/

class BarDerived : public Bar {
 public:

  virtual void run(std::string type) {
    std::cout << "Running " << type << std::endl;
  }

  void simpleRun(SimpleFooImplPtr<BarDerived> base) {
    run("simple");
  }

  void cachedRun(CachedFooImplPtr<BarDerived> derived) {
    run("derived");
    derived->cache();
  }
};

/* CLASS Baz ******************************************************************/

class Baz {
 public:
};

/* CLASS Foo ******************************************************************/

class Foo {
 public:
  Foo(FooImplPtr impl) : _impl(std::move(impl)) {}

  virtual void run() {
    _impl->run();
  }

 private:
  FooImplPtr _impl;
};

/* FUNCTION main **************************************************************/

int main(int argc, char **argv) {
  Foo fooBarDerivedS(std::make_shared<SimpleFooImpl<BarDerived>>());
  fooBarDerivedS.run();

  Foo fooBarDerivedD(std::make_shared<CachedFooImpl<BarDerived>>());
  fooBarDerivedD.run();

  Foo fooBazS(std::make_shared<SimpleFooImpl<Baz>>());
  fooBazS.run();

  Foo fooBazD(std::make_shared<CachedFooImpl<Baz>>());
  fooBazD.run();

  return 0;
}

You can map the names as:

  • Foo: Evaluator
  • Bar: DecodableModel
  • Baz: any subclass of ProbabilisticModel that is not subclass of DecodableModel

Any concrete method has to be implemented as cache(), and all virtual methods need to be used as with run().

I'm happy to say that I found a better solution for this problem!

In the above solution, there is lots of replication (method prototypes in both general and specialization classes, redeclaration of non-inhitered methods, etc). Using the same principle which made this solution work (SFINAE), it's possible to produce only methods that need different versions twice (one for Decodables and othr for non-Decodables):

Here is another minimal example to show the concept:

#include <memory>
#include <iostream>
#include <type_traits>

/* CLASS FooImpl **************************************************************/

class FooImpl : public std::enable_shared_from_this<FooImpl> {
 public:
  virtual void run() = 0;
};

using FooImplPtr = std::shared_ptr<FooImpl>;

/* TYPEDEF Helpers ************************************************************/

// Forward declarations
class Bar;

template<typename T>
using is_bar = typename std::enable_if<std::is_base_of<Bar,T>::value,T>::type;

template<typename T>
using not_bar = typename std::enable_if<!std::is_base_of<Bar,T>::value,T>::type;

/* CLASS SimpleFooImpl ********************************************************/

template<typename T>
class SimpleFooImpl : public FooImpl {
 public:
  virtual void run() override {
    // We need an auxiliar, non-virtual method, to be templetized
    runImpl();
  }

 private:
  // Implementation methods need to have a template, so that
  // SFINAE will make only one to be produced and ignore others
  template<typename U = T>
  void runImpl(not_bar<U>* dummy = nullptr) {
    // We use a dummy argument to produce a malformed
    // template (`not_bar<U>` does not exist) and make this
    // implementation be ignored
    std::cout << "Doing nothing... (not son of Bar)" << std::endl;
  }  

  template<typename U = T>
  void runImpl(is_bar<U>* dummy = nullptr) {
    // Same as above. It will not be produced if `is_bar<U>`
    // is malformed. As both conditions are mutual exclusive,
    // just one of the versions will be generated by the compiler
    U u{};
    u.simpleRun(std::static_pointer_cast<SimpleFooImpl<U>>(
      this->shared_from_this()));
  }

  // With a slight change we could add a non-exclusive test
  // and have more type-specific implementations
};

template<typename T>
using SimpleFooImplPtr = std::shared_ptr<SimpleFooImpl<T>>;

/* CLASS CachedFooImpl ********************************************************/

template<typename T>
class CachedFooImpl : public SimpleFooImpl<T> {
 public:
  virtual void run() override {
    // Same procedure as before
    runImpl();
  }

  // Derived class getter
  int cache() {
    return _cache;
  }

 private:
  // Unhappily, it's not possible to reuse. But if the method
  // is so simple, some replication will not harm...
  template<typename U = T>
  void runImpl(not_bar<U>* dummy = nullptr) {
    std::cout << "Doing nothing... (not son of Bar)" << std::endl;
  }  

  template<typename U = T>
  void runImpl(is_bar<U>* dummy = nullptr) {
    U u{};
    u.cachedRun(std::static_pointer_cast<CachedFooImpl<U>>(
      this->shared_from_this()));
  }

  // Derived class attribute
  int _cache = 0;
};

template<typename T>
using CachedFooImplPtr = std::shared_ptr<CachedFooImpl<T>>;

/* CLASS Bar ******************************************************************/

class Bar {
 public:
  virtual void run(std::string type) = 0;
};

/* CLASS BarDerived ***********************************************************/

class BarDerived : public Bar {
 public:

  virtual void run(std::string type) {
    std::cout << "Running " << type << std::endl;
  }

  void simpleRun(SimpleFooImplPtr<BarDerived> base) {
    run("simple");
  }

  void cachedRun(CachedFooImplPtr<BarDerived> derived) {
    run("cached");
    derived->cache();
  }
};

/* CLASS Baz ******************************************************************/

class Baz {
 public:
};

/* CLASS Foo ******************************************************************/

class Foo {
 public:
  Foo(FooImplPtr impl) : _impl(std::move(impl)) {}

  virtual void run() {
    _impl->run();
  }

 private:
  FooImplPtr _impl;
};

/* FUNCTION main **************************************************************/

int main(int argc, char **argv) {
  //BarDerived bard{};

  Foo fooBarDerivedS(std::make_shared<SimpleFooImpl<BarDerived>>());
  fooBarDerivedS.run();

  Foo fooBarDerivedD(std::make_shared<CachedFooImpl<BarDerived>>());
  fooBarDerivedD.run();

  Foo fooBazS(std::make_shared<SimpleFooImpl<Baz>>());
  fooBazS.run();

  Foo fooBazD(std::make_shared<CachedFooImpl<Baz>>());
  fooBazD.run();

  return 0;
}

Wow! It's a better solution indeed.
And it is easier to understand than the previous solution