jeudi 29 octobre 2015

Composing Interfaces in C++

Suppose I wish to write a C++ function that takes as its argument an object that conforms to two unrelated interfaces. There are several ways this might be done; I list three below along with my critiques. (Please bear with me until I get to the actual question).

1) As a template function

template<typename T> void foo(const T& x) {
    x.fromInterfaceA();
    x.fromInterfaceB();
}

This is perhaps the best of the 'standard' solutions, but ensuring that x really inherits from A and B requires additional static_asserts, type_traits, etc., which is clumsy and long-winded. Also, the user is forced to constantly write template code where in fact there is no really generic behavior desired.

2) By having the function accept a specific derived class

struct A {
    // ...
    virtual void fromInterfaceA() = 0;
};

struct B {
    // ...
    virtual void fromInterfaceB() = 0;
};

struct AB : public A, public B {};

void foo(const AB& ab) {}

We are forced to create a new class just to express the composition of two other classes, and, even worse, foo() is not able to accept arguments that might inherit from A and B via a different route, e.g. through an intermediate class. We could quickly have a taxonomic nightmare, where there are multiple micro-interfaces.

3) By having foo accept a separate argument for each type, and passing the object multiple times.

void foo(const A& xAsA, const B& xAsB) {}
int main() {AB ab; foo(ab, ab);}

This is the most flexible solution (since it would also fit well with a composition-over-inheritance approach), yet it is also confusing, obscures the fact that foo is supposed to operate on a single object, and allows foo to accept two separate objects, possibly leading to pathological behavior.

In Haskell, we can compose type constraints in the way I'm talking about. It would be nice if syntax existed in C++ that allowed the same thing; I can see, though, that this would have serious implications for the internals of the class mechanism. We could write a template class, something like this rough sketch:

template<typename T1, typename T2> class Composition
{
    public:

        template<typename T> Composition(T& t) : t1_(t), t2_(t) {}

        operator T1&() {return t1_;}
        operator T2&() {return t2_;}
        template<typename T> T& as() {return operator T&();}

    private:

        T1& t1_;
        T2& t2_;
};

which appears to offer the advantages of the template solution, (1), while quarantining the actual template code.

My question is: are there any standard design patterns that deal with this issue? Or, alternatively, is there a paradigm where the problem goes away, while still allowing the same kind of flexibility I'm talking about? I'm also interested in more general critiques or thoughts on the discussion above.

Aucun commentaire:

Enregistrer un commentaire