Search code examples
c++templatesinheritancemultiple-inheritance

Multiple templated interface inheritance name hiding


I have a class with a templated method which issue requests.

Here is the class

#include <iostream>

struct Client
{
    template<class Request, class Response>
    void sendRequest(const Request& q , Response& a)
    {
        std::cout << q << " " << a << "\n";
    }
};

Now, it's unusual for a method to issue unknown types of requests. Most methods are going to send a few request types, and I'd like these methods to use an interface in order to make the code testable and clearly express their dependencies on those requests.

But we all know that we can't make templated methods virtual.

So I want to create an interface for using the class for specific requests and responses.

Here the interface

template<class Q, class A, class ... Rest>
struct IClient : public IClient<Rest ...>
{
    using IClient<Rest ...>::sendRequest;
    virtual void sendRequest(const Q& , A&) = 0;

    ~IClient() = default;
};

template<class Q, class A>
struct IClient<Q, A>
{
    virtual void sendRequest(const Q& , A&) = 0;
    ~IClient() = default;
};

The idea is that there is one specialization which takes 2 parameter types and defines a sendRequest method for those 2 types. I pushed the template parameters to the class so the method can be virtual.

The generic class derives from the specialization created using the first 2 arguments of the template argument pack, so that we can define multiple request and response types.

A function can use it like void foo(IClient<ReqA, RespA> client); or void foo(IClient<ReqA, RespA, ReqB, RespB> client); and so on based on the requests it needs to do.

At this point it's really clear from the calling code which requests are going to be made by the function. If I were passing the Client directly I would lose that information (in addition to not be able to mock the client).

So far so good.

From now on let's use this as our testing code

void foo(IClient<int, char, int, int, double, float>& client)
{
    int i = 10;
    char c = 'd';
    double d = 3.14;
    float f = 100.5f;
    client.sendRequest(i, c);
    client.sendRequest(i, i);
    client.sendRequest(d, f);
}

int main()
{
    Client client;
    ClientAdapter<int, char, int, int, double, float> adapter(client);
    foo(adapter);
}

But I don't want to manually create a class that simply forwards the call to the Client, my time is more valuable than that. Let's have the compiler work hard for me!

Here the code to create an adapter which takes the client and abides to the interface.

template<class Q, class A, class ... Rest>
struct ClientAdapter: public IClient<Q, A, Rest ...>, public ClientAdapter<Rest ...>
{
    using ClientAdapter<Rest ...>::sendRequest;

    Client& client;
    ClientAdapter(Client& c) : ClientAdapter<Rest...>(c), client(c)
    {

    }

    void sendRequest(const Q& q, A& a) override
    {
        client.sendRequest(q, a);
    }

    ~ClientAdapter() = default;
};

template<class Q, class A>
struct ClientAdapter<Q, A> : public IClient<Q, A>
{
    Client& client;
    ClientAdapter(Client& c) : client(c) { }

    void sendRequest(const Q& q, A& a) override
    {
        client.sendRequest(q, a);
    }

    ~ClientAdapter() = default;
};

This time the idea is to define an adapter for every single interface given the request and response types used by the user, derive from them and thus be able to use the class in place of the interface.

Here the dream is broken.

 In function 'int main()':
 70:55: error: cannot declare variable 'adapter' to be of abstract type 'ClientAdapter<int, char, int, int, double, float>' 
 30:8: note: because the following virtual functions are pure within 'ClientAdapter<int, char, int, int, double, float>': 
25:18: note: void IClient<Q, A>::sendRequest(const Q&, A&) [with Q = double; A = float]
25:18: note: void IClient<Q, A>::sendRequest(const Q&, A&) [with Q = double; A = float]
17:18: note: void IClient<Q, A, Rest>::sendRequest(const Q&, A&) [with Q = int; A = int; Rest = {double, float}]

If I define the adapter as

template<class Q, class A, class ... Rest>
struct ClientAdapter: public ClientAdapter<Q, A>, public ClientAdapter<Rest ...>

I get a

In function 'int main()':
71:16: error: invalid initialization of reference of type 'IClient<int, char, int, int, double, float>&' from expression of type 'ClientAdapter<int, char, int, int, double, float>'
56:6: note: in passing argument 1 of 'void foo(IClient<int, char, int, int, double, float>&)'

which I think is because ClientAdapter is not a derived type from IClientAdapter (but only derives all the classes in its hierarchy before it).

At this point my knowledge of C++ stops (I started fairly recently) and I have no idea how to fix the problem.

To me the first error doesn't make sense, because while it's true that those functions are pure in the IClient interface, I "name unhidden" all of them into the class by using Derived::method, so they can be resolved when calling ClientAdapter.

I'm also wondering why the method appear 2 times with the same parameters in the error.

How can I get this code to compile and do the right thing?

Here a wandbox example to see the errors and try and fiddle with it.

Thank you


Solution

  • A simplified reproduction:

    struct A {
        virtual void foo() = 0;
    };
    
    struct B {
        virtual void foo() {}
    };
    
    struct C : A, B {
        using B::foo;
    };
    
    C c; // error: C is abstract
    

    The problem is that using B::foo doesn't override A::foo.

    How does it relate to your code?

    template<class Q, class A, class ... Rest>
    struct ClientAdapter: public IClient<Q, A, Rest ...>, public ClientAdapter<Rest ...>
    {   
        using ClientAdapter<Rest ...>::sendRequest; ...        
        void sendRequest(const Q& q, A& a) override ...
    };
    

    You inherit IClient with several pure virtual methods but override only one. The rest are still pure and make ClientAdapter abstract. The fact that the recursively-reduced ClientAdapter overrides sendRequest methods in recursively-reduced IClient doesn't matter, as these are two different instances of IClient.

    If you inherit IClient virtually in all cases, the problem goes away because of inheritance via dominance (Live demo). Note that this mode of inheritance provokes a warning C4250 in MSVC. You can safely ignore it.