【问题标题】:CRTP to avoid dynamic polymorphismCRTP 避免动态多态性
【发布时间】:2010-09-20 16:58:23
【问题描述】:

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

【问题讨论】:

    标签: c++ templates virtual crtp


    【解决方案1】:

    有两种方法。

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

    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
    

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

    【讨论】:

    • 我想强调的是,not_derived_from_base 不是源自base,也不是源自base...
    • 其实my_type/your_type里面的foo()的声明是不需要的。 codepad.org/ylpEm1up(导致堆栈溢出)——有没有办法在编译时强制定义 foo ? -- 好的,找到了解决方案:ideone.com/C6Oz9 -- 也许你想在你的答案中更正它。
    • 你能解释一下在这个例子中使用 CRTP 的动机是什么吗?如果 bar 将被定义为 template void bar(T& obj) { obj.foo(); },那么任何提供 foo 的类都可以。因此,根据您的示例,CRTP 的唯一用途似乎是在编译时指定接口。这就是它的目的吗?
    • @Dean Michael 实际上,即使 foo 未在 my_type 和 your_type 中定义,示例中的代码也会编译。如果没有这些覆盖,base::foo 会被递归调用(和 stackoverflows)。所以也许你想更正你的答案,如 cookie451 所示?
    • @mezhaka:是的,Dean Michael 的示例是不完整的,因为正如您所展示的,它可以在没有 CRTP 的情况下更简洁地实现。但是添加一个template&lt;class T&gt; bar(base2&lt;T&gt; &amp;obj) { obj.quux(); }——即具有不同bar()实现的第二个基类——并且CRTP的实用性变得明显。
    【解决方案2】:

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

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

    【讨论】:

    • @fizzer 我已经阅读了你建议的部分,但仍然不明白 template double sum(Matrix& A);比 template double sum(Whatever& A); 买你
    • @AntonDaneyko 在基实例上调用时,会调用基类的总和,例如具有默认实现的“形状区域”,就好像它是正方形一样。在这种情况下,CRTP 的目标是解决最衍生的实现,“梯形的面积”等,同时仍然能够将梯形称为形状,直到需要衍生行为。基本上,只要您通常需要 dynamic_cast 或虚拟方法。
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      带有严格签名检查的 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&lt;DerivedClass&gt; 一般来说,会做你所期望的。如果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。所以, 即使我们只为接受布尔值的签名设置调度,has_foo&lt;Derived&gt; 将解析为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>
      

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

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

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

      【讨论】:

        【解决方案5】:

        This Wikipedia 回答有你所需要的一切。即:

        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 正在做的事情

        【讨论】:

        • 实际上,每个模板实例化的 Base 重复是一种错觉,因为(除非您仍然有一个 vtable)编译器将为您将 base 和派生的存储合并到一个结构中。函数指针调用也被编译器优化(static_cast 部分)。
        • 顺便说一下,你对CRTP的分析是错误的。应该是: 记忆:没有,正如迪恩迈克尔所说。运行时:一个(更快的)静态函数调用,而不是虚拟函数,这是练习的重点。 static_cast 不做任何事情,它只是允许代码编译。
        • 我的意思是,基本代码将在所有模板实例中重复(您所说的合并)。类似于只有一个依赖于模板参数的方法的模板;基类中的其他一切都更好,否则它会被多次拉入(“合并”)。
        • 基础中的每个 方法 都将为每个派生重新编译。在每个实例化方法不同(因为 Derived 的属性不同)的(预期)情况下,这不一定算作开销。但与(普通)基类中的复杂方法调用子类的虚拟方法的情况相比,它会导致更大的整体代码大小。此外,如果您将实用程序方法放在 Base 中,它们实际上根本不依赖于 ,它们仍然会被实例化。也许全局优化会在一定程度上解决这个问题。
        • 经过多层 CRTP 的调用会在编译期间在内存中扩展,但可以通过 TCO 和内联轻松收缩。那么,CRTP 本身并不是真正的罪魁祸首,对吧?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-03-25
        相关资源
        最近更新 更多