首页 > 解决方案 > 模板化的虚拟成员函数是被禁止的,有替代方案吗?

问题描述

假设我有这样的事情:

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

其中InnerBase是多态基类:

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 { ... }
};

到目前为止一切顺利,如果我想添加重载,我只需要将它们添加到InnerBaseand InnerDerived

在某些时候,事实证明我也需要自定义外部类,所以我希望能够编写如下内容:

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;
};

但标准不允许这样做,该标准禁止模板化的虚拟成员函数。相反,我被迫像这样一一编写所有重载:

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;
};

这是很多样板代码,这意味着如果我想添加另一个重载,我需要修改 4 个类而不是仅仅 2 个(以及两倍的文件)。假设我需要不止一级的间接,这很容易达到 6、8 等,这使得添加新的重载完全不切实际。

我做错了吗?有没有更清洁的方法来做到这一点?

我能想到的唯一选择是泄漏innerBase指向基类的指针,以便它可以doSend()直接调用,这将允许保持send()模板化,但这两者都会破坏封装并防止OuterDerived添加自己的send()重载以及添加自己的重载围绕调用的逻辑doSend()...

标签: c++templatesinheritancevirtual-functions

解决方案


这是很多样板代码,这意味着如果我想添加另一个重载,我需要修改 4 个类而不是仅仅 2 个(以及两倍的文件)。

虽然多层次的继承有点难闻,但天真的解决方案可能是最好的:添加一个派生类,该派生类转发InnerBase并派生自该类:

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 { /* ... */ }
}

现在你只修改了 3 个类。但是,假设您想完全避免键入所有这些重载。然后,您可以使用可变参数模板获得所有过度设计。我们的目标是完成这项工作:

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 { /* ... */ }
};

首先,如果您使用 C++17,可变参数外基非常容易。如果你不是,那么你仍然可以在 C++11 中使用递归继承来执行此操作,这不太漂亮且编译速度较慢:

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

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

制作的第一部分OuterInnerProxyT是确定哪些Ts inOuterBase<Ts...>有重载InnerBase。一个简单的方法是使用 SFINAE 将每种类型转换为一个完整或空的元组,然后将它们混合在一起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>()...));
};

接下来,定义覆盖send单一类型的类。我们可以使用虚拟继承来确保它们具有共同的OuterBaseInnerBase*. 如果我没有在每个级别显式调用虚拟基础,MSVC 会对我大喊大叫,但它不会被调用:

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); }
};

最后,我们可以使用一点偏特化来组合它们:

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>;

添加一些额外的样板来初始化虚拟基础并解除send重载,就是这样:

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 { /* ... */ }
};

演示: https ://godbolt.org/z/7z7Exo

但有一个警告:这在 msvc 2019、clang 11 和 gcc 10.1 上运行良好。但无论出于何种原因,它在 godbolt 上的 gcc 10.2 上出现了段错误。我正在尝试在我的 PC 上构建 gcc 10.2 以找出原因,如果它不是编译器错误。调试时会更新。


推荐阅读