CRTP避免动态多态

时间:2008-11-04 16:07:47

标签: c++ templates virtual crtp

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

5 个答案:

答案 0 :(得分:133)

有两种方式。

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

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

因此,在函数中结合结构/接口定义和编译时类型推导允许您进行静态调度而不是动态调度。这是静态多态的本质。

答案 1 :(得分:18)

我一直在寻找CRTP的正确讨论。 Todd Veldhuizen的Techniques for Scientific C++是一个很好的资源(1.3)和许多其他高级技术,如表达模板。

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

答案 2 :(得分:1)

我不得不抬头CRTP。但是,完成后,我发现了一些关于Static Polymorphism的内容。我怀疑这是你问题的答案。

事实证明ATL非常广泛地使用了这种模式。

答案 3 :(得分: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 接受 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 的方法,它接受一个 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 允许我们只分派给具有 完全匹配的签名。

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

编写一个宏,为非重载成员函数创建一个调度程序 会很简单,但是制作一个支持重载函数的函数会 更具挑战性。如果有人愿意贡献,我会欢迎

答案 4 :(得分:-3)

This维基百科的答案满足您的一切需求。即:

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静态多态的开销是:

  • 内存:每个模板实例化的重复基础
  • 运行时:一个函数指针调用+无论static_cast正在做什么