类层次结构的虚拟方法的CRTP参数



我正试图将CRTP类型的参数传递给一个虚拟方法。因此,虚拟方法将需要成为一个模板。然而,C++不允许这样做(还没有?),因为这意味着vtable的大小——编译器实现动态调度的常见方式——在所有源都被编译并链接之前是未知的。(我在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在另一篇SO帖子中也遇到了同样的问题(不过这是一个答案,而不是一个问题)。用户建议为每个专业化声明和定义虚拟方法,但在派生类中,在一个额外的模板中实现实际逻辑,即非虚拟方法,然后将调用从虚拟方法转发到该模板方法:

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宏

#define CRTP_SPECIALIZATIONS_LIST(X) X(A) X(B) // lists all specializations, here A and B

以生成虚拟基类和派生类中的方法。问题是,如果CRTP层次结构是在CRTP.hpp中定义的,并且虚拟基类和派生类是在其他源文件中声明/定义的,那么宏是";被泄漏">通过标题将其发送到包含它的所有翻译单元。有没有更优雅的方法来解决这个问题?是否有一种实现相同目标的模板方式,可能是使用可变模板类型?

非常感谢你的帮助。问候,

伊曼纽尔

众所周知,您可以使用std::variant来实现一个免费的访问者:

using MyVariant =
std::variant<std::reference_wrapper<const CRTPBase<A>>,
std::reference_wrapper<const CRTPBase<B>>,
// ...
>
struct VirtualBase
{
virtual ~VirtualBase() { }
virtual void accept_crtp(MyVariant) = 0;
};
struct VirtualDerived : VirtualBase
{
void accept_crtp(MyVariant var) override
{
std::visit([/*this*/](const auto& crtp){ /*...*/ }, var);
}
};

如果您编写了一个具有不同accept_crtp()重载的CRTP库,这些重载都委托给派生类的方法,则该派生类方法可以是一个模板。CRTP基地也可以用来实现一个虚拟基地:

// declare virtual interface
struct VirtualBase
{
virtual ~VirtualBase() { }
virtual void accept_crtp(const CRTPBase<A> &o) = 0;
virtual void accept_crtp(const CRTPBase<B> &o) = 0;
};
// implement virtual interface by delegating to derived class generic method
template<typename DerivedType>
struct CRTPDerived : VirtualBase
{
using derived_type = DerivedType;
virtual void accept_crtp(const CRTPBase<A> &o)
{ static_cast<derived_type*>(this)->accept_any_crtp(o); }
virtual void accept_crtp(const CRTPBase<B> &o)
{ static_cast<derived_type*>(this)->accept_any_crtp(o); }
};
// implement generic method
struct VirtualDerived : CRTPDerived<VirtualDerived>
{
private:
template<typename T>
void accept_any_crtp(const CRTPBase<T> &o) {
/* one logic to handle any CRTPBase<T> */
}
};

我找到了一个方便的问题解决方案。它的伸缩性很好,这意味着代码量随着虚拟方法的数量线性增长(而不是虚拟方法的数量乘以CRTP类的数量)。此外,我的解决方案在编译时解析了CRTPBase<T>的实际类型;除了虚拟方法调用之外,没有动态调度。感谢Ulrich Eckhardt在VirtualBase的类层次结构中使用CRTP的想法为我指明了正确的方向。

我将描述如何用一种方法来解决这个问题。然后可以对每种方法重复该过程。其思想是VirtualBase中为每个具体类型的CRTPBase<T>生成一个纯虚拟方法,并在所有派生类中生成这些方法的实现。在编译时生成方法的问题是模板不允许我们生成方法名称。这里的技巧是利用重载语义并使用标记类型来执行标记调度。

让我按照这个例子来解释。考虑到CRTP层次结构(注意,我为了演示目的对其进行了轻微更改)

template<typename Actual>
struct CRTPBase
{
using actual_type = Actual;
actual_type & actual() { return *static_cast<actual_type*>(this); }
const actual_type & actual() const {
return *static_cast<const actual_type*>(this);
}
void foo() const { actual().foo(); }
int bar(int i) const { return actual().bar(i); }
void baz(float x, float y) { actual().baz(x, y); }
};
struct A : CRTPBase<A>
{
void foo() const { }
int bar(int i) const { return i + 'A'; }
void baz(float x, float y) { }
};
struct B : CRTPBase<B>
{
void foo() const { }
int bar(int i) const { return i + 'B'; }
void baz(float x, float y) { }
};

我们想要在类层次结构VirtualBase中声明一个接受CRTPBase<T>的任何子类作为参数的虚拟方法bark()。我们创建了一个标记类型bark_t的helper来启用过载解析。

struct VirtualBase
{
private:
virtual void operator()(bark_t, const A&) const = 0;
virtual void operator()(bark_t, const B&) const = 0;
public:
template<typename T>
void bark(const T &o) const { operator()(bark_t{}, o); }
};

模板方法是通用的,并且由于重载解析而调用正确的operator()。此处使用标记类型来选择正确的实现。(我们希望支持多种方法,而不仅仅是bark()。)

接下来,我们使用CRTP:在派生类中定义operator()的实现

template<typename Actual>
struct VirtualCRTP : VirtualBase
{
using actual_type = Actual;
actual_type & actual() { return *static_cast<actual_type*>(this); }
const actual_type & actual() const {
return *static_cast<const actual_type*>(this);
}
void operator()(bark_t{}, const A &o) const override { actual()(bark_t{}, o); }
void operator()(bark_t{}, const B &o) const override { actual()(bark_t{}, o); }
};

注意,该实现调用静态类型为Actual的某个方法operator()。我们接下来需要在VirtualBase:的实现中实现这一点

struct VirtualDerivedX : VirtualCRTP<VirtualDerivedX>
{
template<typename T>
void operator()(bark_t, const T &o) { /* generic implementation goes here */ }
};
struct VirtualDerivedY : VirtualCRTP<VirtualDerivedY>
{
template<typename T>
void operator()(bark_t, const T &o) { /* generic implementation goes here */ }
};

在这一点上,你可能会想";我们在这里收获了什么&";。到目前为止,我们需要为每个实际类型的CRTPBase<T>编写一个方法operator()。只有在VirtualBaseVirtualCRTP中,但仍然比我们想要写的更多。简洁的是,我们现在可以生成方法operator(),包括VirtualBase中的纯虚拟声明和VirtualCRTP中的实现。为此,我定义了一个通用的助手类。我把这个助手类的完整代码和示例放在Godbolt上。

我们可以使用这个helper类来声明新的虚拟方法,这些方法将类型列表的实例以及其他参数作为第一个参数。文中还讨论了参数和方法的CCD_ 30性质。

struct bark_t : const_virtual_crtp_helper<bark_t>::
crtp_args<const A&, const B&>::
args<> { };
struct quack_t : virtual_crtp_helper<quack_t>::
crtp_args<const A&, const B&>::
args<int, float> { };
struct roar_t : const_virtual_crtp_helper<roar_t>::
crtp_args<A&, B&>::
args<const std::vector<int>&> { };
/*----- Virtual Class Hierarchy taking CRTP parameter ------------------------*/
struct VirtualBase : bark_t::base_type
, quack_t::base_type
, roar_t::base_type
{
virtual ~VirtualBase() { }
/* Declare generic `bark()`. */
using bark_t::base_type::operator();
template<typename T>
void bark(const T &o) const { operator()(bark_t{}, o); }
/* Declare generic `quack()`. */
using quack_t::base_type::operator();
template<typename T>
void quack(const T &o, int i, float f) { operator()(quack_t{}, o, i, f); }
/* Declare generic `roar()`. */
using roar_t::base_type::operator();
template<typename T>
void roar(T &o, const std::vector<int> &v) const { operator()(roar_t{}, o, v); }
};
template<typename Actual>
struct VirtualCRTP : VirtualBase
, bark_t::derived_type<Actual>
, quack_t::derived_type<Actual>
, roar_t::derived_type<Actual>
{ };
struct VirtualDerivedX : VirtualCRTP<VirtualDerivedX>
{
private:
/* Implement generic `bark()`. */
friend const_virtual_crtp_helper<bark_t>;
template<typename T>
void operator()(bark_t, const T&) const { /* generic bark() goes here */ }
/* Implement generic `quack()`. */
friend virtual_crtp_helper<quack_t>;
template<typename T>
void operator()(quack_t, const T&, int, float) { /* generic quack() goes here */ }
/* Implement generic `roar()`. */
friend const_virtual_crtp_helper<roar_t>;
template<typename T>
void operator()(roar_t, T&, const std::vector<int>&) const { /* generic roar() goes here */ }
};
struct VirtualDerivedY : VirtualCRTP<VirtualDerivedY>
{
private:
/* Implement generic `bark()`. */
friend const_virtual_crtp_helper<bark_t>;
template<typename T>
void operator()(bark_t, const T&) const { /* generic bark() goes here */ }
/* Implement generic `quack()`. */
friend virtual_crtp_helper<quack_t>;
template<typename T>
void operator()(quack_t, const T&, int, float) { /* generic quack() goes here */ }
/* Implement generic `roar()`. */
friend const_virtual_crtp_helper<roar_t>;
template<typename T>
void operator()(roar_t, T&, const std::vector<int>&) const { /* generic roar() goes here */ }
};

在这个例子中,我为我想要实现的三个方法声明了三个helper类型。base_typeVirtualBase引入了纯虚拟方法,derived_type<Actual>导入了这些方法的实现。为此,我使用虚拟继承来解决出现的可怕的钻石;)

一个缺点是必须在派生类中将virtual_crtp_helper类型声明为friend。也许有人知道如何避免这种情况?

总结:要将方法添加到类层次结构中,必须使用

  1. 使用virtual_crtp_helper<T>const_virtual_crtp_helper<T>为方法声明一个助手类型
  2. VirtualBase继承此类型的base_type,并将该方法定义为泛型模板
  3. VirtualCRTP<Actual>从辅助对象类型的derived_type<Actual>继承
  4. 对于每个派生类,在模板化和标记的operator()中实现实际逻辑

我很高兴听到您的想法,并期待改进。

伊曼纽尔

最新更新