CRTP避免动态多态


Answers:


139

有两种方法。

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

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

因此,在函数中结合使用结构/接口定义和编译时类型推导,可以执行静态分配而不是动态分配。这是静态多态性的本质。


15
绝佳答案
Eli Bendersky

5
我想强调一点,not_derived_from_base它不是源自base,也不是源自base……
大约

3
实际上,不需要my_type / your_type中的foo()声明。codepad.org/ylpEm1up(导致堆栈溢出)-有没有办法在编译时强制执行foo的定义?-好的,找到了一个解决方案:ideone.com/C6Oz9-也许您想在答案中更正它。
cooky451'3

3
您能否向我解释在此示例中使用CRTP的动机是什么?如果将bar定义为template <class T> void bar(T&obj){obj.foo(); },那么任何提供foo的类都可以。因此,根据您的示例,CRTP的唯一用途似乎是在编译时指定接口。那是为了什么?
Anton Daneyko

1
@Dean Michael实际上,即使在my_type和your_type中未定义foo的情况下,示例中的代码也会编译。没有这些重写,base :: foo将被递归调用(和stackoverflows)。所以也许您想纠正cooky451给出的答案?
Anton Daneyko

18

我一直在寻找关于CRTP的不错的讨论。Todd Veldhuizen的《科学C ++技术》是此(1.3)和许多其他高级技术(如表达式模板)的重要资源。

另外,我发现您可以在Google图书中阅读Coplien最初撰写的大多数C ++ Gems文章。也许情况仍然如此。


@fizzer我已经阅读了您建议的部分,但仍然不理解template <class T_leaftype> double sum(Matrix <T_leaftype>&A)是什么?与template <class Whatever> double sum(Whatever&A)相比买了你;
Anton Daneyko

@AntonDaneyko在基实例上调用时,基类的总和被调用,例如,“形状区域”具有默认实现,就好像它是一个正方形一样。在这种情况下,CRTP的目标是解决最派生的实现,例如“梯形区域”等,同时仍然能够将梯形称为形状,直到需要派生的行为为止。基本上,只要您通常需要dynamic_cast虚拟方法。
约翰·P


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

  • 内存:每个模板实例的基本副本
  • 运行时:一个函数指针调用+ static_cast在做什么

4
实际上,每个模板实例化Base的重复是一种错觉,因为(除非您仍然有一个vtable)编译器会为您合并base和派生的存储到单个结构中。函数指针调用也由编译器优化(static_cast部分)。
院长迈克尔

19
顺便说一句,您对CRTP的分析是不正确的。应该是:记忆:什么都没有,就像院长迈克尔说的那样。运行时:一个(更快)的静态函数调用,而不是虚拟的,这是整个练习的重点。static_cast不执行任何操作,仅允许代码进行编译。
Frederik Slijkerman,

2
我的观点是,基本代码将在所有模板实例中重复(您所说的非常融合)。类似于仅使用一种依靠template参数的方法来制作模板;在基类中,其他所有内容都更好,否则将其多次拉入(“合并”)。
user23167

1
基础中的每个方法将针对每个派生方法再次编译。在每种实例化方法都不同的(预期)情况下(由于Derived的属性不同),这不必算作开销。但是,与(普通)基类中的复杂方法调用子类的虚拟方法的情况相比,它可能导致更大的总体代码大小。另外,如果将实用程序方法放在实际上不依赖于<Derived>的Base <Derived>中,它们仍将被实例化。也许全局优化可以解决该问题。
greggo '16

贯穿CRTP多层的调用将在编译期间在内存中扩展,但可以通过TCO和内联轻松地收缩。那么,CRTP本身并不是真正的罪魁祸首,对吗?
约翰·P
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.