c++ - 模板化的虚拟成员函数是被禁止的,有替代方案吗?
问题描述
假设我有这样的事情:
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
and 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
是确定哪些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
单一类型的类。我们可以使用虚拟继承来确保它们具有共同的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 上运行良好。但无论出于何种原因,它在 godbolt 上的 gcc 10.2 上出现了段错误。我正在尝试在我的 PC 上构建 gcc 10.2 以找出原因,如果它不是编译器错误。调试时会更新。
推荐阅读
- c - 参数中的指针,&符号?
- scala - 使用 Mockito 监视带有对象参数的方法会导致 NullPointerException
- java - 给定一个字符串,如果它包含超过 4 个字符,则返回字符串中的第一个字符,否则返回最后一个字符
- resteasy - 如何修复 quarkus 反应端点中的“无法执行选项”?
- javascript - Window.postMessage 事件侦听器未在慢速网络上触发
- rubygems - ID25/rails_emoji_picker gem 不支持暂存和开发环境
- swift - 当集合视图中只有 6 个项目时,请求全局索引 6 的索引路径
- python-3.x - 试图理解 Python 的 AES 方法
- bash - 从期望脚本调用 bash shell 脚本失败:“没有这样的文件或目录”
- android - 在项目中添加 BluringView