Eternal
Eternal

Reputation: 2907

Templated virtual member functions are forbidden, are there alternatives?

Let's say I have something like this:

class Outer
{
public:
    void send(A a) { ... }
    template <typename MessageType>
    void send(MessageType message) { innerBase->doSend(message); }
private:
    InnerBase* innerBase;
};

where InnerBase being a polymorphic base class:

class InnerBase
{
public:
    virtual void doSend(B b) = 0;
    virtual void doSend(C c) = 0;
    virtual void doSend(D d) = 0;
};

class InnerDerived : public InnerBase
{
public:
    virtual void doSend(B b) override { ... }
    virtual void doSend(C c) override { ... }
    virtual void doSend(D d) override { ... }
};

So far so good, and if I want to add overloads I just need to add them to InnerBase and InnerDerived.

At some point, it turns out I need to make the outer class customizable too, so I'd like to be able to write something like this :

class OuterBase
{
public:
    template <typename MessageType>
    virtual void send(MessageType message) = 0;
};

class OuterDerived : public OuterBase
{
public:
    void send(A a) { ... }
    template <typename MessageType>
    virtual void send(MessageType message) override { innerBase->send(message); }
private:
    InnerBase* innerBase;
};

But that's not allowed by the standard, which forbids templated virtual member functions. Instead I'm forced to write all the overloads one by one like this :

class OuterBase
{
public:
    virtual void send(A a) = 0;
    virtual void send(B b) = 0;
    virtual void send(C c) = 0;
    virtual void send(D d) = 0;
};

class OuterDerived : public OuterBase
{
public:
    virtual void send(A a) override { ... }
    virtual void send(B b) override { innerBase->doSend(b); }
    virtual void send(C c) override { innerBase->doSend(c); }
    virtual void send(D d) override { innerBase->doSend(d); }
private:
    InnerBase* innerBase;
};

That's a lot of boilerplate code, and that means that if I ever want to add another overload I need to modify 4 classes instead of just 2 (and twice as many files). Assuming I need more than one level of indirection, this could easily go up to 6, 8, etc, making the addition of new overloads totally impractical.

Am I doing this wrong? Is there a cleaner way to do this?

The only alternative I can think of would be to leak the innerBase pointer to the base class so that it can call doSend() directly, which would allow to keep send() templated, but that both breaks encapsulation and prevents OuterDerived from adding its own send() overloads as well as from adding its own logic around the calls to doSend()...

Upvotes: 4

Views: 151

Answers (3)

n. m. could be an AI
n. m. could be an AI

Reputation: 119857

You don't actually need to duplicate code in OuterDerived, it is enough to have all Message classes to derive from a common base.

class InnerBase
{
public:
    virtual void doSend(Message* m) = 0;
};

class OuterBase : public InerBase
{
public:
    void doSend(MessageBase* m) override {
        m->sendme(innerBase);
    }
private:
    InnerBase* innerBase;
};

This is a standard double dispatch technique. sendme needs to be virtual.

class MessageBase {
public:
   virtual void sendme(InnerBase* innerBase) = 0;
};

class MessageA : public MessageBase {
public:
   void sendme(InnerBase* innerBase) override {
      // this chooses the correct overload
      innerBase->send(*this);
   }
};

Now messageA doesn't need to be an actual message, it could be a wrapper around one, and that wrapper can be a template:

template <class Message>
class MessageWrapper : public MessageBase
{
public:
   void sendme(InnerBase* innerBase) override {
      // this chooses the correct overload
      innerBase->send(message);
   }
 private:
   Message message;
};

Now doSend is independent of the actual message type, messages know nothing about either inner or outer classes, outer classes know nothing about specific messages, and only innet classes (each of them) keep the knowledge about specific messages (each of them). So you still need NxM send functions, but in exactly one place, in the inner class hierarchy.

Upvotes: 0

parktomatomi
parktomatomi

Reputation: 4079

That's a lot of boilerplate code, and that means that if I ever want to add another overload I need to modify 4 classes instead of just 2 (and twice as many files).

While multiple levels of inheritance kind of smells bad, the naive solution might be the best here: add one derived class that forwards to InnerBase and derive from that:

class OuterInnerProxy : public OuterBase
{
public:
    OuterInnerProxy(InnerBase* innerBase) : innerBase(innerBase) {}
    virtual void send(B b) override { innerBase->doSend(b); }
    virtual void send(C c) override { innerBase->doSend(c); }
    virtual void send(D d) override { innerBase->doSend(d); }
private:
    InnerBase* innerBase;
};

class OuterDerived : public OuterInnerProxy
{
public:
    OuterDerived(InnerBase* innerBase) : innerBase(innerBase) {}
    virtual void send(A a) override { /* ... */ }
}

Now you're only modifying 3 classes. But let's say you wanted to avoid typing all those overloads at all. Then you can get all overengineery with variadic templates. Our goal will be to make this work:

using OuterBaseT = OuterBase<A, B, C, D>;
using OuterInnerProxyT = OuterInnerProxy<OuterBaseT>; // overrides B, C, D
struct OuterDerived : OuterInnerProxyT {
    OuterDerived(InnerBase* innerBase);
    virtual void send(A a) override { /* ... */ }
};

First, the variadic outer base is pretty easy if you're using C++17. If you're not, then you can still do this in C++11 with recursive inheritance, which is less pretty and slower to compile:

template <typename T>
class OuterBaseSingle
{
public:
    virtual void send(T t) = 0;
};

template <typename... Ts>
class OuterBase : OuterBaseSingle<Ts>...
{
public:
    using OuterBaseSingle<Ts>::send...;
};

The first part of making OuterInnerProxyT is to determine which Ts in OuterBase<Ts...> have overloads in InnerBase. A simple way to do that is to use SFINAE to convert each type into a full or empty tuple, then mash them together with std::tuple_cat:

template <typename T, typename = void>
struct InnerBaseOverload {
    using Type = std::tuple<>;
};
template <typename T>
struct InnerBaseOverload<T, decltype(std::declval<InnerBase>().doSend(std::declval<T>()))> {
    using Type = std::tuple<T>;
 };
template <typename T>
struct InnerBaseOverloads;
template <typename...Ts>
struct InnerBaseOverloads<OuterBase<Ts...>> {
    using Type = decltype(std::tuple_cat(std::declval<typename InnerBaseOverload<Ts>::Type>()...));
};

Next, define classes that override send for a single type. We can use virtual inheritance to make sure they have a common OuterBase and InnerBase*. MSVC yells at me if I don't explicitly invoke the virtual base at every level, but it won't get called:

class OuterInnerProxyBase {
public:
    OuterInnerProxyBase(InnerBase* innerBase) : innerBase(innerBase) { }
    InnerBase* innerBase;
};

template <typename T, typename... Ts>
class OuterInnerProxySingle : public virtual OuterInnerProxyBase, public virtual OuterBase<Ts...> {
public:
    using OuterBase<Ts...>::send;
    OuterInnerProxySingle() : OuterInnerProxyBase(nullptr) { }
    void send(T t) override { OuterInnerProxyBase::innerBase->doSend(t); }
};

Finally, we can combine them using a little partial specialization:

template <typename TOuterBase, typename TOverloadTuple>
class OuterInnerProxyImpl;

template <typename... TOuterArgs, typename... TOverloads>
class OuterInnerProxyImpl<OuterBase<TOuterArgs...>, std::tuple<TOverloads...>> : public OuterInnerProxySingle<TOverloads, TOuterArgs...>... { };

template <typename T>
using OuterInnerProxy = OuterInnerProxyImpl<T, typename InnerBaseOverloads<T>::Type>;

Add some extra boilerplate to initialize the virtual base and lift the send overloads, And that's it:

using OuterBaseT = OuterBase<A, B, C, D>;
class OuterDerived : public OuterInnerProxy<OuterBaseT> {
public:
    OuterDerived(InnerBase* inner) : OuterInnerProxyBase(inner) { }
    using OuterBaseT::send; 
    virtual void send(A a) override { /* ... */ }
};

Demo: https://godbolt.org/z/7z7Exo

One caveat though: This runs happily on msvc 2019, clang 11, and gcc 10.1. But for whatever reason, it segfaults on gcc 10.2 on godbolt. I'm trying to build gcc 10.2 it on my PC to figure out why, if it's not a compiler bug. Will update when I debug it.

Upvotes: 1

Superstar64
Superstar64

Reputation: 1

If you want to have a fixed number of overloads on send, then I'd suggest add each overload to the vtable as your already doing. Then for each implementation, you just forward each vtable definition to a template definition defined in each derived class.

If you want an unlimited number of overloads, unfortunately your out of luck here. To do this safely, this requires a language feature called higher rank or rank-n types and higher rank types require some level of type erasure like Java's generics. So instead you have to do manual type erasure, where you pass in a pointer and a dictionary, and be careful to not mix up your types.

For example, consider this pseudo C++:

// stuff you can do with A
erasure template <pointer A>
struct Dictonary {
    std::function<A()> create;
    std::function<A(A)> increment;
};

class OuterBase
{
public:
     erasure template <pointer A> virtual void send(A a, Dirtonary<A>) = 0;
};

and after manual type erasure:

// stuff you can do with A
struct Dictonary {
    std::function<void*()> create;
    std::function<void*(void*)> increment;
};

class OuterBase
{
public:
     virtual void send(void* a, Dirtonary) = 0;
};

Also, if all your the functions you need to use for A involve this you can define a new base class rather then rely on dictionary passing.

Upvotes: 0

Related Questions