假设我有这样的东西:
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 { ... }
};
到目前为止一切顺利,如果我想添加重载,我只需要将它们添加到InnerBase
和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()
调用添加自己的逻辑......
这是很多样板代码,这意味着如果我想添加另一个重载,我需要修改 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...>
中的哪些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
的类。我们可以使用虚拟继承来确保它们具有共同的OuterBase
和InnerBase*
。如果我不在每个级别显式调用虚拟基础,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
函数,但恰好在一个地方,即内部类层次结构中。