90

如何在 C++ 中使用 CRTP 来避免虚拟成员函数的开销?

4

5 回答 5

142

有两种方法。

第一个是通过为类型结构静态指定接口:

template <class Derived>
struct base {
  void foo() {
    static_cast<Derived *>(this)->foo();
  };
};

struct my_type : base<my_type> {
  void foo(); // required to compile.
};

struct your_type : base<your_type> {
  void foo(); // required to compile.
};

第二个是避免使用对基址的引用或指向基址的指针,并在编译时进行布线。使用上面的定义,您可以拥有如下所示的模板函数:

template <class T> // T is deduced at compile-time
void bar(base<T> & obj) {
  obj.foo(); // will do static dispatch
}

struct not_derived_from_base { }; // notice, not derived from base

// ...
my_type my_instance;
your_type your_instance;
not_derived_from_base invalid_instance;
bar(my_instance); // will call my_instance.foo()
bar(your_instance); // will call your_instance.foo()
bar(invalid_instance); // compile error, cannot deduce correct overload

因此,结合函数中的结构/接口定义和编译时类型推导,您可以进行静态分派而不是动态分派。这就是静态多态的本质。

于 2008-11-04T19:02:56.813 回答
18

我自己一直在寻找关于 CRTP 的体面讨论。Todd Veldhuizen 的 Technicals for Scientific C++是此 (1.3) 和许多其他高级技术(如表达式模板)的重要资源。

此外,我发现您可以在 Google 书籍中阅读 Coplien 的大部分原始 C++ Gems 文章。也许情况仍然如此。

于 2008-11-04T17:49:27.943 回答
1

我不得不查找CRTP。然而,在这样做之后,我发现了一些关于Static Polymorphism的东西。我怀疑这是您问题的答案。

事实证明,ATL相当广泛地使用了这种模式。

于 2008-11-04T16:11:39.637 回答
0

具有严格签名检查的 CRTP/SFINAE 静态调度

这种静态调度的解决方案使用了 CRTP 和 SFINAE,这并不新鲜。这个解决方案的独特之处在于它还强制执行严格的签名检查,这允许我们以与动态分派对虚函数相同的方式静态分派重载方法。

首先,让我们先看看使用 SFINAE 的传统解决方案的局限性。以下内容摘自 Ben Deane 的 CppCon 2016 Lightning Talk “虚拟函数的静态替代方案,使用表达式 SFINAE”。

#define SFINAE_DETECT(name, expr)                                       \
  template <typename T>                                                 \
  using name##_t = decltype(expr);                                      \
  template <typename T, typename = void>                                \
  struct has_##name : public std::false_type {};                        \
  template <typename T>                                                 \
  struct has_##name<T, void_t<name##_t<T>>> : public std::true_type {};

// detect CommonPrefix(string)
SFINAE_DETECT(common_prefix,
              declval<T>().CommonPrefix(std::string()))

使用上面的代码,模板实例化has_complete<DerivedClass> 通常会按照您的预期进行。如果DerivedClass有一个名为 Complete接受 a的方法std::string,则结果类型将为 std::true_type.

当你想重载一个函数时会发生什么?

template <class Derived>
struct Base {
    std::string foo(bool);
    std::string foo(int);
    ...
};

struct Derived : public Base<Derived>
{
    std::string foo(int);
};

在这种情况下,Derived确实有一个名为的方法foo,它接受 a bool因为bool可以隐式转换为int. 因此,即使我们只为接受 bool 的签名设置调度,has_foo<Derived>也会解析为std::true_type,并且调用将被调度到Derived::foo(int)。这是我们想要的吗?可能不会,因为这不是虚函数的工作方式。如果两个签名完全匹配,一个函数只能覆盖一个虚函数。我建议我们创建一个以相同方式运行的静态调度机制。

template <template <class...> class Op, class... Types>
struct dispatcher;

template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};

template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
  : std::experimental::detected_or_t<
    typename dispatcher<Op, Types...>::type, Op, T> {};

template <template <class...> class Op, class... Types>
using dispatcher_t = typename dispatcher<Op, Types...>::type;

这很好,但仅此一项并不能强制执行签名检查。要执行严格的签名检查,我们必须正确定义模板模板参数 Op。为此,我们将使用std::integral_constant一个成员函数指针。看起来是这样的:

template <class T>
using foo_op_b = std::integral_constant<std::string(T::*)(bool), &T::foo>;

template <class T>
using foo_op_i = std::integral_constant<std::string(T::*)(int), &T::foo>

以这种方式定义我们Op的 s 允许我们仅调度具有完全签名匹配的方法。

// Resolves to std::integral_constant<std::string(T::*)(bool), &Derived::foo>
using foo_bool_ic = dispatcher_t<foo_op_b, Derived, Defaults>;

// Resolves to std::integral_constant<std::string(T::*)(int), &Defaults::foo>
using foo_int_ic = dispatcher_t<foo_op_i, Derived, Defaults>;

现在让我们把它们放在一起。

#include <iostream>
#include <experimental/type_traits>
#include <string>

template <template <class...> class Op, class... Types>
struct dispatcher;

template <template <class...> class Op, class T>
struct dispatcher<Op, T> : std::experimental::detected_t<Op, T> {};

template <template <class...> class Op, class T, class... Types>
struct dispatcher<Op, T, Types...>
  : std::experimental::detected_or_t<
    typename dispatcher<Op, Types...>::type, Op, T> {};

template <template <class...> class Op, class... Types>
using dispatcher_t = typename dispatcher<Op, Types...>::type;


// Used to deduce class type from a member function pointer
template <class R, class T, class... Args>
auto method_cls(R(T::*)(Args...)) -> T;


struct Defaults {
    std::string foo(bool value) { return value ? "true" : "false"; }
    std::string foo(int  value) { return value ? "true" : "false"; }

    // Ensure that the class is polymorphic so we can use dynamic_cast
    virtual ~Defaults() {};
};

template <class Derived>
struct Base : Defaults {
    template <class T>
    using foo_op_b = std::integral_constant<std::string(T::*)(bool), &T::foo>;

    template <class T>
    using foo_op_i = std::integral_constant<std::string(T::*)(int), &T::foo>;

    std::string foo(bool value) {
        auto method = dispatcher_t<foo_op_b, Derived, Defaults>::value;
        auto *target = dynamic_cast<decltype(method_cls(method)) *>(this);
        return (target->*method)(value);
    }

    std::string foo(int value) {
        auto method = dispatcher_t<foo_op_i, Derived, Defaults>::value;
        auto *target = dynamic_cast<decltype(method_cls(method)) *>(this);
        return (target->*method)(value);
    }
};

struct Derived : Base<Derived> {
    std::string foo(bool value) { return value ? "TRUE" : "FALSE"; }
};

int main() {
    Derived d;
    std::cout << dynamic_cast<Base<Derived> *>(&d)->foo(true) << std::endl; // TRUE
    std::cout << dynamic_cast<Base<Derived> *>(&d)->foo(1) << std::endl;    // true
}

编写一个为非重载成员函数创建分派器的宏很简单,但创建一个支持重载函数的宏会更具挑战性。如果有人愿意贡献,我会欢迎添加。

于 2021-01-29T20:12:10.420 回答
-5

这个维基百科的答案有你需要的一切。即:

template <class Derived> struct Base
{
    void interface()
    {
        // ...
        static_cast<Derived*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        Derived::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};

虽然我不知道这实际上给你买了多少。虚函数调用的开销是(当然取决于编译器):

  • 内存:每个虚函数一个函数指针
  • 运行时:一次函数指针调用

而 CRTP 静态多态的开销是:

  • 内存:每个模板实例化的 Base 副本
  • 运行时:一个函数指针调用 + static_cast 正在做的任何事情
于 2008-11-04T16:12:36.250 回答