【问题标题】:CRTP Parameter for Virtual Method of Class Hierarchy类层次结构虚方法的 CRTP 参数
【发布时间】: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&lt;T&gt; 的所有特化。因为我知道特定的专业,所以我可以这样做:

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&lt;T&gt; 的每个特化都有一个虚拟方法,无论是在纯虚拟基类中还是在其所有派生类中。随着CRTPBase&lt;T&gt; 的专业化数量的增加和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&lt;T&gt; 特化的逻辑代码重复,但它仍然需要在虚拟基类和所有派生类中为每个特化显式编写一个方法。

我的问题是:如何减少实施开销?

我考虑过使用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 实际上,它与访问者模式非常相似。但是,我避免在对AB 的任何方法的调用中进行动态调度。感谢您指出 mixin 类型。我不知道这个概念。但我看不到这将如何解决我的问题。我认为它只会将代码重复转移到另一种类型:我仍然有AB,但没有CRTPBase&lt;T&gt; 和新的mixin 类型来操作AB。虚拟方法的签名保持不变,但在内部这些将使用 mixin 类型对 AB 进行操作。恕我直言,没有任何收获。

标签: c++ templates inheritance virtual crtp


【解决方案1】:

由于所有类型都是已知的,您可以使用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);
    }
};

【讨论】:

  • 好吧,std::variant——或者更准确地说是std::visit()——也实现了动态调度,很像没有vtable的虚拟继承。因此,这与不使用 CRTP 而是首先使用常规虚拟继承一样好。
【解决方案2】:

如果您编写具有不同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> */
    }
};

【讨论】:

  • 非常有趣的想法。代码中的 VirtualDerived 不应该继承自 CRTPDerived&lt;VirtualDerived&gt; 吗?
  • 是的,当然!
【解决方案3】:

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

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

让我解释一下这个例子。给定 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 中声明一个虚方法bark(),它接受CRTPBase&lt;T&gt; 的任何子类作为参数。我们创建了一个辅助标记类型 bark_t 来启用重载解析。

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&lt;T&gt; 的实际类型编写一个方法operator()。仅在VirtualBaseVirtualCRTP 中,但仍然比我们想写的要多。巧妙的是,我们现在可以生成方法operator()VirtualBase 中的纯虚拟声明和VirtualCRTP 中的实现。为此,我定义了一个通用助手类。我把这个帮助类的完整代码和例子放在Godbolt上。

我们可以使用这个帮助类来声明新的虚拟方法,这些方法将类型列表的实例以及其他参数作为第一个参数。它还负责const-ness 的参数和方法。

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

在示例中,我为要实现的三个方法声明了三个辅助类型。 base_type 将纯虚方法引入VirtualBasederived_type&lt;Actual&gt; 导入这些方法的实现。为此,我使用虚拟继承来解决出现的可怕的钻石;)

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

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

  1. 使用virtual_crtp_helper&lt;T&gt;const_virtual_crtp_helper&lt;T&gt; 为该方法声明一个帮助器类型。
  2. VirtualBase 继承此类型的base_type 并将方法定义为通用模板。
  3. VirtualCRTP&lt;Actual&gt; 继承自辅助类型的 derived_type&lt;Actual&gt;
  4. 对于每个派生类,在模板化和标记operator() 中实现实际逻辑。

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

以马内利

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-17
    • 1970-01-01
    相关资源
    最近更新 更多