1

I have the following code:

struct AInterface {
    XXX some_method(/* some params */) = 0;
};

void foo(const AInterface & a) {
    do_work();

    while (x) {
        do_work_2();
        foo_process_input(a);
    }

    return some_value();
}

void foo_process_input(const AInterface & a) {
    do_inefficient(a);
}

However, I recently realized that some objects implementing AInterface are actually following a more specialized interface, so that I could do:

struct BInterface : AInterface {
     YYY some_better_method(/* some other params */) = 0;
};

void foo_process_input(const BInterface & b) {
    do_efficient(b);
}

in order to improve the performance of my code in certain cases. Note: the signatures of some_method and some_better_method do not allow using one over the other.

However, I'm wondering how I should modify foo in order to accommodate this change and avoid duplicating the code. As I can see, it seems I have two options:

  • Templatize foo to accept both interfaces
  • Rely on dynamic_cast
template <typename Interface>
void foo(const Interface & a_or_b) {
    do_work();

    while (x) {
        do_work_2();
        foo_process_input(a_or_b);
    }

    return some_value();
}

The templatized solution requires less changes, but has the disadvantage that the compiler is going to duplicate all the do_work code which does not really depend on the input. In addition, this makes uglier errors, as the actual type of a_or_b is only checked upon calling foo_process_input, which being an internal of foo could be confusing for the user.

void foo(const AInterface & a_or_b) {
    do_work();

    auto * binterface = dynamic_cast<BInterface*>(&a_or_b);

    while (x) {
        do_work_2();
        if (binterface) foo_process_input(*binterface);
        else foo_process_input(a_or_b);
    }

    return some_value();
}

The dynamic_cast solution should work and not cost too much in terms of performance, but I've always been wary of runtime type checking, and it makes me wonder if I could somehow improve on the interface definition.

Do you have any suggestion on how to approach this problem?

2

As you noticed, using a dynamic_cast is fairly fragile and inelegant.

If some_method() and some_better_method() have the same interface (i.e. satisfy the Liskov substition principle), there is no particular reason why you should use separate methods. Instead, overriding the virtual method when a better implementation is available would seem more desirable.

Virtual dispatch allows the target object to select the appropriate implementation. That is one of the main points of OOP. By using a dynamic cast and therefore selecting the implementation yourself, you give up most of the advantages of polymorphism.

As you mention, the two methods have incompatible signatures, although clearly they must be doing something similar. In many cases, it is possible to define an interface that allows to perform the same behaviour but without mentioning concrete types. It is then possible to use the object adapter pattern to adapt your original interfaces to the common interface. This is also known as type erasure, and e.g. is the approach used by std::function<…> to adapt any function-like object or function pointer to a common interface.

In your specific example, this would mean that foo_process_input() should not call do_inefficient(a) or do_efficient(b), but adapter.do_whatever_the_adapter_decides(). You would then have an DoStuffAdapter interface with an InefficientDoStuffAdapter that can wrap AInterface instances, and an EfficientDoStuffAdapter that wraps BInterface instances.

The problem then becomes selecting the correct adapter for a given object. In a sense, this just pushes the problem down one level, and it would be tempting to use use a dynamic cast again. Instead:

  • If the caller of foo() knows the real type of your object, they can wrap it there, e.g. foo(InefficientDoStuffAdapter(real_object)) or foo(EfficientDoStuffAdapter(real_object)).

    This is possible whenever a template<…> foo() would be possible, since your suggested template solution will only work if the real type of the target object is statically known to the compiler.

  • If a dependency from the AInterface instances to the adapters is OK in your domain model, every AInterface instance should be able to adapt itself to the DoStuffAdapter interface. This would mean a method like this, that can then be overridden if a better adapter is available:

    virtual std::unique_ptr<DoStuffAdapter> adapt_to_do_stuff() const {
      return make_unique<InefficientDoStuffAdapter>(*this);
    }
    
  • If such a dependency is not OK, at least make AInterface visitable, so that the visitor pattern may be used to select the appropriate implementation. This shares some problems of a dynamic cast (the visitor pattern is essentially a “safe“ dynamic cast). In particular, you need to know all relevant types at design time, and cannot add new cases dynamically. But this pattern ultimately leaves the decision to select the appropriate implementation in the hand of the target object, not in the hands of the dependent code. Solution sketch:

    struct Visitor;
    
    struct AInterface {
      virtual XXX some_method(/* some params */) = 0;
      virtual void accept_visitor(Visitor& v) const;
    };
    
    struct BInterface : Ainterface {
      virtual YYY some_better_method(/* some other params */) = 0;
      virtual void accept_visitor(Visitor& v) const override;
    };
    
    struct Visitor {
      virtual void visit_a(AInterface const& a) = 0;
      virtual void visit_b(BInterface const& b) = 0;
    };
    
    void AInterface::accept_visitor(Visitor& v) const {
      v.visit_a(*this);
    }
    
    void BInterface::accept_visitor(Visitor& v) const {
      v.visit_b(*this);
    }
    

    Then the correct Adapter may be selected as:

    struct SelectAdapterVisitor : Visitor {
      std::unique_ptr<DoStuffAdapter> adapter = nullptr;
    
      void visit_a(AInterface const& a) {
        adapter = make_unique<InefficientDoStuffAdapter>(a);
      }
    
      void visit_b(BInterface const& b) {
        adapter = make_unique<EfficientDoStuffAdapter>(a);
      }
    };
    
    std::unique_ptr<DoStuffAdapter> select_adapter(AInterface const& a_or_b) {
       SelectAdapterVisitor v;
       a_or_b.accept_visitor(v);
       return std::move(v.adapter);
    }
    

Some problems can be solved through both templates and type erasure techniques. Type erasure is slightly non-obvious to implement and awkward to use, so a simple template solution is usually much better. The template-based suggestion in your question is such a simple solution.

If however the template solution cannot be simple, e.g. because more complicated metaprogramming with SFINAE etc. is required, then a bit of type erasure is much easier to understand than lots of templates. Another scenario to prefer type erasure is in a large project with long compilation times. Whereas templates require the templated code to be present in header files and therefore must recompile the same source code over and over again, polymorphism-based techniques like type erasure isolate different compilation units from each other.

  • I like the adapter idea, thanks! In particular, in my case it's probably the easiest to have both foo(AInterface) and foo(BInterface). These two foos simply wrap around a lambda or something the correct foo_process_input call, and pass it down to some foo_impl(std::function). – Svalorzen Apr 8 '17 at 21:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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