【问题标题】:CRTP vs. virtual function as an interface or mixinCRTP 与虚函数作为接口或混合
【发布时间】:2023-02-02 19:30:22
【问题描述】:

我想知道如果我从不从基类调用函数(即虚拟分派),那么使用 CRTP 是否比虚拟函数多态性有任何好处?

这是示例代码。反汇编可以在https://godbolt.org/z/WYKaG5bbG找到。

struct Mixin {
  virtual void work() = 0;
};

template <typename T>
struct CRTPMixin {
  void call_work() {
    static_cast<T*>(this)->work();
  }
};

struct Parent {};
struct Child : Parent, Mixin, CRTPMixin<Child> {
  int i = 0;
  void work() override {
    i ++;
  }
};

Child child;
Mixin& mixin = child;


int main() {
  child.work();
  mixin.work();
  child.call_work();
}

我发现如果我从child或通过CRTPMixin接口调用虚函数work,反汇编代码是一样的,只有静态的call。如果我在Mixin&amp; mixin = child 上调用该函数,则会发生虚拟分派,并且会为此操作生成更多指令。

我的问题是,如果我正在设计接口/mixin 类型的结构,我将只调用派生类,而不是基类,那么在任何情况下 CRTP 会比 virutal 函数方法受益更多吗?

谢谢!

【问题讨论】:

  • 你的例子混合了这两个概念,你真的应该有虚拟方法或 ctrp/mixin 才能真正看到区别。所以你要么有一个从 Mixin 派生的子类,要么从 CrtpMixin 派生,而不是两者。最后,静态多态性必须产生没有虚拟调用的代码。
  • @PepijnKramer 通常 mixins 调用派生类的一些函数,否则这样的“mixin”可以只是普通的基类,不需要虚函数或 CRTP。
  • @sklott 你是对的。

标签: c++ polymorphism virtual-functions crtp static-polymorphism


【解决方案1】:

如果您总是只从派生类调用,那么 CRTP 比虚函数要好得多。 直接调用函数不仅比通过虚拟分派更快,而且还允许函数内联和其他优化。

从 C++23 开始,我们可以比以前更简单地实现 CRTP。来自https://en.cppreference.com/w/cpp/language/crtp 的示例

#include <cstdio>
 
#ifndef __cpp_explicit_this_parameter // Traditional syntax
 
template <class Derived>
struct Base { void name() { (static_cast<Derived*>(this))->impl(); } };
struct D1 : public Base<D1> { void impl() { std::puts("D1::impl()"); } };
struct D2 : public Base<D2> { void impl() { std::puts("D2::impl()"); } };
 
void test()
{
    Base<D1> b1; b1.name();
    Base<D2> b2; b2.name();
    D1 d1; d1.name();
    D2 d2; d2.name();
}
 
#else // C++23 alternative syntax; https://godbolt.org/z/KbG8bq3oP
 
struct Base { void name(this auto& self) { self.impl(); } };
struct D1 : public Base { void impl() { std::puts("D1::impl()"); } };
struct D2 : public Base { void impl() { std::puts("D2::impl()"); } };
 
void test()
{
    D1 d1; d1.name();
    D2 d2; d2.name();
}
 
#endif
 
int main()
{
    test();
}

【讨论】:

  • 感谢您的见解!如果我只从派生类调用,为什么要使用虚拟分派?从反汇编中,我看到了用于调用派生类上的虚函数和 CRTP 调用的相同指令。
  • 如果你从对象调用,它会直接调用,但如果你使用指针或引用,它会使用虚拟调度(如果你的类没有声明final),因为你可以有派生类而编译器不知道这一点。
  • Base&lt;D1&gt; b1; b1.name();错了...
【解决方案2】:

我最近在开发项目时获得了一些经验。我在问题中给出的示例确实没有显示使用CRTP和虚函数的区别。但是,在某些情况下,虚函数无法真正匹配 CRTP 静态多态性的性能。

假设我有一个 Base,它有一个 invoke 公共 API,它的行为可以通过在派生类中实现 run 方法来修改。

代码如下所示:

#include <iostream>

using namespace std;

/* CRTP */
template<class Derived>
struct CRTPBase {
  void run() {
    static_cast<Derived *>(this)->run();
  }

  void invoke() {
    this->run();
  }
};

struct CRTPDerived : public CRTPBase<CRTPDerived> {
  void run() {
    cout << "RUN
";
  }

  void invoke() {
    this->run();
  }
};

/* virtual */
struct VirtualBase {
  virtual void run() {};

  void invoke() {
    this->run();
  }
};

struct VirtualDerived : public VirtualBase {
  void run() override {
    cout << "RUN
";
  }
};

int main() {
  CRTPDerived cd;
  cd.invoke();
  VirtualDerived vd;
  vd.invoke();
}

可在https://godbolt.org/z/rnxd9r6Kr进行拆解

在这种情况下,即使我们调用具体对象 VirtualDerived 上的方法而不是使用 VirtualBase 的指针或引用,虚拟调度仍然会发生,并且需要另外四条指令来调用 invoke 函数。在这种情况下,无法避免使用虚函数时的开销。

【讨论】:

    猜你喜欢
    • 2016-07-17
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    • 2017-04-16
    • 2016-07-16
    • 2012-08-08
    • 2012-09-20
    • 1970-01-01
    相关资源
    最近更新 更多