禁止使用模板化虚拟成员功能,还有其他选择吗?



假设我有这样的东西:

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

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

在某些时候,事实证明我也需要使外部类可自定义,所以我希望能够编写这样的东西:

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()调用添加自己的逻辑......

这是很多样板代码,这意味着如果我想添加另一个重载,我需要修改 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的第一部分是确定OuterBase<Ts...>中的哪些TsInnerBase中具有重载。一个简单的方法是使用 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上运行良好。但无论出于何种原因,它在 gcc 10.2 上在 godbolt 上出现段错误。我正在尝试在我的 PC 上构建 gcc 10.2 以找出原因,如果它不是编译器错误。调试时会更新。

如果你想在send上有一个固定数量的重载,那么我建议像你已经做的那样将每个重载添加到 vtable。然后,对于每个实现,只需将每个 vtable 定义转发到每个派生类中定义的模板定义。

如果你想要无限数量的重载,不幸的是你在这里不走运。为了安全地做到这一点,这需要一种称为更高秩或秩 n 类型的语言功能,而更高等级的类型需要某种级别的类型擦除,如 Java 的泛型。因此,您必须执行手动类型擦除,在其中传入指针和字典,并注意不要混淆类型。

例如,请考虑以下伪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;
};

手动类型擦除后:

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

此外,如果你需要用于 A 的所有函数都涉及this你可以定义一个新的基类,而不是依赖于字典传递。

你实际上不需要在OuterDerived中复制代码,让所有Message类都从一个共同的基派生就足够了。

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

这是一种标准的双重调度技术。sendme需要是虚拟的。

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

现在,messageA 不需要是实际的消息,它可以是围绕一个消息的包装器,并且该包装器可以是一个模板:

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

现在doSend独立于实际的消息类型,消息对内部或外部类一无所知,外部类对特定消息一无所知,并且只有 innet 类(每个类)保留有关特定消息(每个消息)的知识。因此,您仍然需要 NxMsend函数,但恰好在一个地方,即内部类层次结构中。

最新更新