什么是奇怪的重复模板模式(CRTP)?

Posted

技术标签:

【中文标题】什么是奇怪的重复模板模式(CRTP)?【英文标题】:What is the curiously recurring template pattern (CRTP)? 【发布时间】:2010-11-13 15:30:07 【问题描述】:

不参考一本书,任何人都可以通过代码示例为CRTP提供一个很好的解释吗?

【问题讨论】:

阅读关于 SO 的 CRTP 问题:***.com/questions/tagged/crtp。这可能会给你一些想法。 @sbi:如果他这样做,他会找到自己的问题。这会奇怪地反复出现。 :) 顺便说一句,在我看来这个词应该是“奇怪地递归”。我是不是理解错了? 克雷格:我想你是;它“奇怪地反复出现”,因为它被发现在多种情况下出现。 【参考方案1】:

简而言之,CRTP 是指类A 有一个基类,它是类A 本身的模板特化。例如

template <class T> 
class X...;
class A : public X<A> ...;

奇怪地反复出现,不是吗? :)

现在,这给了你什么?这实际上使X 模板能够成为其专业化的基类。

例如,您可以像这样制作一个通用的单例类(简化版)

template <class ActualClass> 
class Singleton

   public:
     static ActualClass& GetInstance()
     
       if(p == nullptr)
         p = new ActualClass;
       return *p; 
     

   protected:
     static ActualClass* p;
   private:
     Singleton()
     Singleton(Singleton const &);
     Singleton& operator = (Singleton const &); 
;
template <class T>
T* Singleton<T>::p = nullptr;

现在,为了使任意类 A 成为单例,您应该这样做

class A: public Singleton<A>

   //Rest of functionality for class A
;

所以你看到了吗?单例模板假定其对任何类型 X 的特化将从 singleton&lt;X&gt; 继承,因此其所有(公共的、受保护的)成员都可以访问,包括 GetInstance! CRTP 还有其他有用的用途。例如,如果您想计算您的类当前存在的所有实例,但想将此逻辑封装在一个单独的模板中(具体类的想法非常简单 - 有一个静态变量,在 ctors 中递增,在 dtors 中递减)。尝试将其作为练习!

另一个有用的例子,对于 Boost(我不确定他们是如何实现它的,但 CRTP 也可以)。 想象一下,您只想为您的课程提供运算符&lt;,但自动为它们提供运算符==

你可以这样做:

template<class Derived>
class Equality

;

template <class Derived>
bool operator == (Equality<Derived> const& op1, Equality<Derived> const & op2)

    Derived const& d1 = static_cast<Derived const&>(op1);//you assume this works     
    //because you know that the dynamic type will actually be your template parameter.
    //wonderful, isn't it?
    Derived const& d2 = static_cast<Derived const&>(op2); 
    return !(d1 < d2) && !(d2 < d1);//assuming derived has operator <

现在可以这样使用了

struct Apple:public Equality<Apple> 

    int size;
;

bool operator < (Apple const & a1, Apple const& a2)

    return a1.size < a2.size;

现在,您还没有为Apple 明确提供运算符==?但你有它!你可以写

int main()

    Apple a1;
    Apple a2; 

    a1.size = 10;
    a2.size = 10;
    if(a1 == a2) //the compiler won't complain! 
    
    

如果您只为Apple 编写运算符==,您可能会写得更少,但想象一下Equality 模板不仅会提供==,还会提供&gt;&gt;=、@987654344 @ 等。您可以将这些定义用于多个类,重用代码!

CRTP 是个好东西 :) HTH

【讨论】:

本帖不提倡单例作为一种好的编程模式,只是作为一个可以通俗理解的说明。imo the-1是没有根据的 @Armen:答案以一种可以清楚理解的方式解释了 CRTP,这是一个很好的答案,感谢您提供这样一个很好的答案。 @Armen:感谢您的精彩解释。我以前有点 t 获得 CRTP,但是平等的例子很有启发性! +1 另一个使用 CRTP 的例子是当你需要一个不可复制的类时:template class NonCopyable protected: NonCopyable() ~NonCopyable() private: NonCopyable(const不可复制&); NonCopyable& 运算符=(const NonCopyable&); ;然后你使用不可复制如下: class Mutex : private NonCopyable public: void Lock() void UnLock() ; @Puppy:单身并不可怕。当其他方法更合适时,它被低于平均水平的程序员过度使用,但它的大多数用法都很糟糕,但这并不会使模式本身变得糟糕。在某些情况下,单例是最好的选择,尽管这种情况很少见。【参考方案2】:

在这里你可以看到一个很好的例子。如果您使用虚拟方法,程序将知道在运行时执行什么。实现 CRTP 编译器决定了编译时间!!!这是一场精彩的表演!

template <class T>
class Writer

  public:
    Writer()   
    ~Writer()   

    void write(const char* str) const
    
      static_cast<const T*>(this)->writeImpl(str); //here the magic is!!!
    
;


class FileWriter : public Writer<FileWriter>

  public:
    FileWriter(FILE* aFile)  mFile = aFile; 
    ~FileWriter()  fclose(mFile); 

    //here comes the implementation of the write method on the subclass
    void writeImpl(const char* str) const
    
       fprintf(mFile, "%s\n", str);
    

  private:
    FILE* mFile;
;


class ConsoleWriter : public Writer<ConsoleWriter>

  public:
    ConsoleWriter()  
    ~ConsoleWriter()  

    void writeImpl(const char* str) const
    
      printf("%s\n", str);
    
;

【讨论】:

你不能通过定义virtual void write(const char* str) const = 0;来做到这一点吗?虽然公平地说,当write 做其他工作时,这种技术似乎非常有用。 使用纯虚方法解决的是运行时而不是编译时的继承问题。 CRTP用于在编译时解决这个问题,因此执行会更快。 尝试创建一个需要抽象 Writer 的普通函数:你不能这样做,因为任何地方都没有名为 Writer 的类,那么你的多态性到底在哪里?这根本不等同于虚函数,而且它的用处要小得多。【参考方案3】:

CRTP 是一种实现编译时多态性的技术。这是一个非常简单的例子。在下面的示例中,ProcessFoo() 正在使用 Base 类接口,Base::Foo 调用派生对象的 foo() 方法,这就是您打算使用虚拟方法执行的操作。

http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e

template <typename T>
struct Base 
  void foo() 
    (static_cast<T*>(this))->foo();
  
;

struct Derived : public Base<Derived> 
  void foo() 
    cout << "derived foo" << endl;
  
;

struct AnotherDerived : public Base<AnotherDerived> 
  void foo() 
    cout << "AnotherDerived foo" << endl;
  
;

template<typename T>
void ProcessFoo(Base<T>* b) 
  b->foo();



int main()

    Derived d1;
    AnotherDerived d2;
    ProcessFoo(&d1);
    ProcessFoo(&d2);
    return 0;

输出:

derived foo
AnotherDerived foo

【讨论】:

在这个例子中添加一个例子来说明如何在基类中实现一个默认的 foo() 也是值得的,如果没有 Derived 实现它,它将被调用。 AKA 将 Base 中的 foo 更改为其他名称(例如 caller()),将新函数 foo() 添加到 cout 的“Base”的 Base。然后在 ProcessFoo 中调用 caller() 这是我最喜欢的答案,因为它也说明了为什么这种模式对ProcessFoo() 函数很有用。 我不明白这段代码的意义,因为使用 void ProcessFoo(T* b) 并且没有 Derived 和 AnotherDerived 实际上派生它仍然可以工作。恕我直言,如果 ProcessFoo 不以某种方式使用模板会更有趣。 @GabrielDevillers 首先,模板化的ProcessFoo() 将适用于实现接口的任何类型,即在这种情况下,输入类型T 应该有一个名为foo() 的方法。其次,为了让非模板化的ProcessFoo 能够处理多种类型,您最终可能会使用我们想要避免的 RTTI。此外,模板化版本为您提供了界面上的编译时间检查。 非常感谢!这是最好的解释。简单的简短示例。类似于此en.cppreference.com/w/cpp/language/crtp【参考方案4】:

这不是一个直接的答案,而是一个CRTP如何有用的例子。


CRTP 的一个很好的具体示例是来自 C++11 的 std::enable_shared_from_this

[util.smartptr.enab]/1

一个类T可以从enable_­shared_­from_­this&lt;T&gt;继承来继承shared_­from_­this成员函数,该成员函数获得一个shared_­ptr实例指向*this

也就是说,从 std::enable_shared_from_this 继承可以在不访问实例的情况下获取指向您的实例的共享(或弱)指针(例如,从您只知道 *this 的成员函数)。

当您需要提供std::shared_ptr 但您只能访问*this 时,它很有用:

struct Node;

void process_node(const std::shared_ptr<Node> &);

struct Node : std::enable_shared_from_this<Node> // CRTP

    std::weak_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;

    void add_child(std::shared_ptr<Node> child)
    
        process_node(shared_from_this()); // Shouldn't pass `this` directly.
        child->parent = weak_from_this(); // Ditto.
        children.push_back(std::move(child));
    
;

您不能直接传递this 而不是shared_from_this() 的原因是它会破坏所有权机制:

struct S

    std::shared_ptr<S> get_shared() const  return std::shared_ptr<S>(this); 
;

// Both shared_ptr think they're the only owner of S.
// This invokes UB (double-free).
std::shared_ptr<S> s1 = std::make_shared<S>();
std::shared_ptr<S> s2 = s1->get_shared();
assert(s2.use_count() == 1);

【讨论】:

【参考方案5】:

如注:

CRTP可用于实现静态多态(类似于动态多态,但没有虚函数指针表)。

#pragma once
#include <iostream>
template <typename T>
class Base

    public:
        void method() 
            static_cast<T*>(this)->method();
        
;

class Derived1 : public Base<Derived1>

    public:
        void method() 
            std::cout << "Derived1 method" << std::endl;
        
;


class Derived2 : public Base<Derived2>

    public:
        void method() 
            std::cout << "Derived2 method" << std::endl;
        
;


#include "crtp.h"
int main()

    Derived1 d1;
    Derived2 d2;
    d1.method();
    d2.method();
    return 0;

输出将是:

Derived1 method
Derived2 method

【讨论】:

很抱歉,static_cast 负责更改。如果您想查看角落案例,即使它不会导致错误,请参见此处:ideone.com/LPkktf 不好的例子。此代码可以在不使用 CRTP 的情况下在没有 vtables 的情况下完成。 vtables 真正提供的是使用基类(指针或引用)来调用派生方法。你应该在这里展示它是如何使用 CRTP 完成的。 在你的例子中,Base&lt;&gt;::method () 甚至没有被调用,你也没有在任何地方使用多态性。 @Jichao,根据@MikeMB 的说明,您应该在Basemethod 和派生类名称methodImpl 中调用methodImpl,而不是method 如果你使用类似的 method() 那么它是静态绑定的,你不需要公共基类。因为无论如何你不能通过基类指针或引用多态地使用它。所以代码应该是这样的: #include template struct Writer void write() static_cast(this)->writeImpl(); ; struct Derived1 : public Writer void writeImpl() std::cout void writeImpl() std::cout 【参考方案6】:

另一个使用 CRTP 的好例子是观察者设计模式的实现。可以这样构造一个小例子。

假设您有一个类date,并且您有一些侦听器类,例如date_drawerdate_reminder 等。侦听器类(观察者) 每当日期更改完成时,主题类date(可观察)应通知他们,以便他们可以完成工作(在某些 格式,提醒特定日期等)。您可以做的是有两个参数化基类observerobservable,您应该从中派生 你的date 和观察者类(在我们的例子中是date_drawer)。观察者设计模式的实现可以参考GOF等经典书籍。这里我们只需要 突出使用CRTP。让我们看看它。 在我们的草案实现中,observer 基类有一个纯虚方法,只要发生状态更改,observable 类就应该调用它, 让我们将此方法称为state_changed。我们来看看这个小的抽象基类的代码。

template <typename T>
struct observer

    virtual void state_changed(T*, variant<string, int, bool>) = 0;
    virtual ~observer() 
;

在这里,我们应该关注的主要参数是第一个参数T*,它将是状态发生变化的对象。第二个参数 将是被改变的领域,它可以是任何东西,甚至你可以省略它,这不是我们主题的问题(在这种情况下它是一个std::variant 3 个字段)。 第二个基类是

template <typename T>
class observable

    vector<unique_ptr<observer<T>>> observers;
protected:
    void notify_observers(T* changed_obj, variant<string, int, bool> changed_state)
    
        for (unique_ptr<observer<T>>& o : observers)
        
            o->state_changed(changed_obj, changed_state);
        
    
public:
    void subscribe_observer(unique_ptr<observer<T>> o)
    
        observers.push_back(move(o));
    
    void unsubscribe_observer(unique_ptr<observer<T>> o)
    

    
;

它也是一个参数类,它依赖于 T* 类型,它与传递给 state_changed 函数内部的对象相同 notify_observers 函数。 仅介绍实际的主题类date 和观察者类date_drawer这里使用了CRTP模式,我们从observable&lt;date&gt;派生出date observable类:class date : public observable&lt;date&gt;

class date : public observable<date>

    string date_;
    int code;
    bool is_bank_holiday;

public:
    void set_date_properties(int code_ = 0, bool is_bank_holiday_ = false)
    
        code = code_;
        is_bank_holiday = is_bank_holiday_;
        //...
        notify_observers(this, code);
        notify_observers(this, is_bank_holiday);
    

    void set_date(const string& new_date, int code_ = 0, bool is_bank_holiday_ = false) 
     
        date_ = new_date; 
        //...
        notify_observers(this, new_date);
    
    string get_date() const  return date_; 
;

class date_drawer : public observer<date>

public:
    void state_changed(date* c, variant<string, int, bool> state) override
    
        visit([c](const auto& x) cout << "date_drawer notified, new state is " << x << ", new date is " << c->get_date() << endl; , state);
    
;

让我们编写一些客户端代码:

date c;
c.subscribe_observer(make_unique<date_drawer>());
c.set_date("27.01.2022");
c.set_date_properties(7, true);

这个测试程序的输出将是。

date_drawer notified, new state is 27.01.2022, new date is 27.01.2022
date_drawer notified, new state is 7, new date is 27.01.2022
date_drawer notified, new state is 1, new date is 27.01.2022

请注意,每当发生状态更改时,使用 CRTP 并将this 传递给通知notify_observers 函数(此处为set_date_propertiesset_date)。允许我们在实际的date_drawer 观察者类中覆盖void state_changed(date* c, variant&lt;string, int, bool&gt; state) 纯虚函数时使用date*,因此我们在其中有date* c(不是observable*),例如我们可以调用一个非虚函数date*(在我们的例子中是get_date) 在state_changed 函数内。 我们可以避免使用 CRTP,因此不参数化观察者设计模式实现,并在任何地方使用observable 基类指针。这样我们可以得到相同的效果,但是在这种情况下,无论何时我们想要使用派生类指针(即使不是很推荐),我们都应该使用 dynamic_cast 向下转换,这会产生一些运行时开销。

【讨论】:

以上是关于什么是奇怪的重复模板模式(CRTP)?的主要内容,如果未能解决你的问题,请参考以下文章

使用奇怪的重复模板模式 (CRTP) 在抽象基类中实现赋值运算符

浅谈 CRTP:奇异递归模板模式

浅谈 CRTP:奇异递归模板模式

CRTP 特征仅适用于模板派生类

C++ 静态多态性 (CRTP) 和使用派生类的 typedef

c++派生类的类型列表