【发布时间】:2022-01-12 07:07:48
【问题描述】:
我正在尝试将 CRTP 类型参数传递给虚拟方法。因此,虚拟方法需要是一个模板。但是,这在 C++ 中是不允许的(还没有?),因为这意味着 vtable 的大小——编译器实现动态调度的常用方式——在所有源都被编译并被链接之前是未知的。 (I found this reasoning during my search on SO.)
然而,在我的特定环境中,CRTP 专业化的数量是有限且已知的。因此,可以为每个特化定义一个虚拟方法重载并在子类中覆盖它们。我准备了一个小型 MWE 来展示我的情况。考虑以下 CRTP 层次结构:
template<typename Actual>
struct CRTPBase
{
using actual_type = Actual;
void foo() { static_cast<actual_type*>(this)->foo(); }
int bar(int i) const { return static_cast<const actual_type*>(this)->bar(i); }
};
struct A : CRTPBase<A>
{
void foo() { /* do something A would do */ }
int bar(int i) const { return i + 1; }
};
struct B : CRTPBase<B>
{
void foo() { /* do something B would do */ }
int bar(int i) const { return i - 1; }
};
接下来,我想用一个虚拟方法定义一个虚拟类层次结构来处理CRTPBase<T> 的所有特化。因为我知道特定的专业,所以我可以这样做:
struct VirtualBase
{
virtual ~VirtualBase() { }
virtual void accept_crtp(const CRTPBase<A> &o) = 0;
virtual void accept_crtp(const CRTPBase<B> &o) = 0;
};
struct VirtualDerived : VirtualBase
{
void accept_crtp(const CRTPBase<A> &o) override { /* much logic to handle A */ }
void accept_crtp(const CRTPBase<B> &o) override { /* similar logic to handle B */ }
};
请注意,CRTPBase<T> 的每个特化都有一个虚拟方法,无论是在纯虚拟基类中还是在其所有派生类中。随着CRTPBase<T> 的专业化数量的增加和VirtualBase 的更多派生类的增加,这种开销很容易变得不成比例。
我想做的,大致如下:
struct VirtualBase
{
virtual ~VirtualBase() { }
template<typename T> virtual void accept_crtp(const CRTPBase<T> &o) = 0;
}
struct VirtualDerived : VirtualBase
{
template<typename T> void accept_crtp(const CRTPBase<T> &o) override {
/* one logic to handle any CRTPBase<T> */
}
};
由于开头提到的原因,这是不可能的。用户 Mark Essel 在another SO post 中遇到了同样的问题(尽管是在答案中,而不是在问题中)。 用户建议为每个特化声明和定义虚拟方法,但在派生类中在附加模板、非虚拟方法中实现实际逻辑,然后将虚拟方法的调用转发到该模板方法:
struct VirtualBase
{
virtual ~VirtualBase() { }
virtual void accept_crtp(const CRTPBase<A> &o) = 0;
virtual void accept_crtp(const CRTPBase<B> &o) = 0;
};
struct VirtualDerived : VirtualBase
{
void accept_crtp(const CRTPBase<A> &o) override { accept_any_crtp(o); }
void accept_crtp(const CRTPBase<B> &o) override { accept_any_crtp(o); }
private:
template<typename T>
void accept_any_crtp(const CRTPBase<T> &o) {
/* one logic to handle any CRTPBase<T> */
}
};
虽然这种方法避免了处理CRTPBase<T> 特化的逻辑代码重复,但它仍然需要在虚拟基类和所有派生类中为每个特化显式编写一个方法。
我的问题是:如何减少实施开销?
我考虑过使用X macro 的形式
#define CRTP_SPECIALIZATIONS_LIST(X) X(A) X(B) // lists all specializations, here A and B
在虚拟基类和派生类中生成方法。问题在于,如果 CRTP 层次结构是在 CRTP.hpp 中定义的,并且虚拟基类和派生类在其他源文件中声明/定义,那么宏是 “被标题泄露”到包含它的所有翻译单元。有没有更优雅的方法来解决这个问题?是否有可能实现相同目标的模板方式,也许是可变参数模板类型?
感谢您的帮助。亲切的问候,
以马内利
【问题讨论】:
-
是否打算将
foo()和bar()隐藏起来,即派生类具有类似命名的成员函数?另外,作为对此的第一反应,我想知道您为什么不简单地创建具有不同实现的虚拟基类。您显示的代码不能证明使用 CRTP 是合理的。最后一点:也许使用 mixin 代替 CRTP 也有帮助。 -
@UlrichEckhardt 是的,阴影是有意的,它不应该损害一般的 CRTP 设计。 why 我需要 CRTP 的原因是为了让编译器能够进行积极的内联。类 A 和 B 提供相同概念实体的不同内存布局,例如就像稀疏矩阵和密集矩阵一样。 A 或 B 将被一些算法在热循环中使用,它们的方法可能会被调用数百万次。
-
不确定这是否有帮助,但我认为您正在做的是“访问者模式”的实现。无论如何,如果您编写一个具有不同
accept_crtp()重载的CRTP 基础,所有这些重载都委托给派生类的方法,那么该派生类的方法可以是一个模板。该 CRTP 基础也可用于实现虚拟基础。 -
@UlrichEckhardt 实际上,它与访问者模式非常相似。但是,我避免在对
A和B的任何方法的调用中进行动态调度。感谢您指出 mixin 类型。我不知道这个概念。但我看不到这将如何解决我的问题。我认为它只会将代码重复转移到另一种类型:我仍然有A和B,但没有CRTPBase<T>和新的mixin 类型来操作A或B。虚拟方法的签名保持不变,但在内部这些将使用 mixin 类型对A或B进行操作。恕我直言,没有任何收获。
标签: c++ templates inheritance virtual crtp