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

What is the curiously recurring template pattern (CRTP)?

提问人:Alok Save 提问时间:11/13/2010 最后编辑:TankorSmashAlok Save 更新时间:9/13/2023 访问量:67724

问:

在不参考一本书的情况下,任何人都可以用代码示例提供一个很好的解释吗?CRTP

C 模板 C++-FAQ CRTP

评论

2赞 sbi 11/13/2010
阅读有关 SO: stackoverflow.com/questions/tagged/crtp 的 CRTP 问题。这可能会给你一些想法。
100赞 Craig McQueen 1/8/2013
@sbi:如果他这样做了,他会找到自己的问题。奇怪的是,这种情况会反复出现。:)
1赞 Craig McQueen 1/8/2013
顺便说一句,在我看来,这个词应该是“好奇地递归”。我是不是误解了意思?
3赞 Gareth McCaughan 4/20/2016
克雷格:我想你是;从某种意义上说,它是“奇怪地反复出现”,因为它被发现在多种情况下出现。

答:

336赞 Armen Tsirunyan 11/13/2010 #1

简而言之,CRTP 是指一个类具有基类,该基类是类本身的模板专用化。例如AA

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

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

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

例如,您可以像这样创建一个通用单例类(简化版本)

#include <iostream>

template <class T>
class Singleton
{
public:
     static T* GetInstance() {
         if ( p == nullptr ) p = new T();
         return p;
     }
protected:
     Singleton() = default;

     Singleton(Singleton const &) = delete;
     Singleton &operator=(const Singleton &) = delete;

private:
     static T *p;
};
template <class T>
T *Singleton<T>::p= nullptr;

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

class A : public Singleton<A> 
{ 
friend Singleton;
private:
    A() = default;
};
A *a0= A::GetInstance();

在这种情况下,不需要 CRTP,如下所示:

class C 
{ 
friend Singleton<C>; 
private: C() = default;
};
C *c1= Singleton<C>::GetInstance();

所以你明白了吗?单一实例模板假定其对任何类型的专用化都将继承自,因此其所有(公共、受保护)成员都可以访问,包括 !CRTP 还有其他有用的用途。例如,如果你想计算你的类当前存在的所有实例,但想将这个逻辑封装在一个单独的模板中(具体类的想法很简单 - 有一个静态变量,在 ctors 中递增,在 dtors 中递减)。试着把它当作一种练习来做!Xsingleton<X>GetInstance

另一个有用的例子,对于 Boost(我不确定他们是如何实现的,但 CRTP 也会这样做)。 想象一下,你只想为你的类提供运算符,但自动为它们提供运算符!<==

你可以这样做:

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

或在模板范围内实现,无需强制转换

template<class T>
class Equality
{
    friend bool operator == (const T& op1, const T& op2)
    { 
        return !(op1 < op2) && !(op2 < op1); 
    }
};

现在你可以像这样使用它了

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! 
    {
    }
}

如果您只为 编写运算符,这似乎会写得更少,但想象一下模板不仅会提供 , , 等。您可以将这些定义用于多个类,重用代码!==AppleEquality==>>=<=

CRTP是一件美妙的事情:)HTH型

评论

77赞 John Dibling 11/14/2010
这篇文章并不提倡单例是一个好的编程,pattern.it 只是把它作为一个可以普遍理解的例证。
3赞 Alok Save 11/16/2010
@Armen:答案以一种可以清楚地理解的方式解释了 CRTP,这是一个很好的答案,谢谢你这么好的答案。
1赞 Paul 4/8/2011
@Armen:谢谢你的精彩解释。我以前有点没有得到 CRTP,但平等的例子很有启发性!+1
1赞 Viren 4/21/2014
使用 CRTP 的另一个例子是当你需要一个不可复制的类时: 模板 <class T> class NonCopyable { protected: NonCopyable(){} ~NonCopyable(){} private: NonCopyable(const NonCopyable&);NonCopyable& 运算符=(const NonCopyable&);};然后你使用不可复制的,如下所示: class Mutex : private NonCopyable<Mutex> { public: void Lock(){} void UnLock(){} };
4赞 Kaiserludi 7/29/2015
@Puppy:辛格尔顿并不可怕。当其他方法更合适时,它被低于平均水平的程序员过度使用,但它的大多数用法都很糟糕,但这并不意味着模式本身很糟糕。在某些情况下,单例是最佳选择,尽管这种情况很少见。
3赞 Jichao 10/11/2013 #2

请注意:

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

评论

1赞 odinthenerd 12/6/2013
对不起,我的错,static_cast负责更改。如果你想看到极端情况,即使它不会导致错误,请参阅这里:ideone.com/LPkktf
34赞 Etherealone 2/7/2014
坏例子。如果不使用 CRTP,可以在没有 s 的情况下完成此代码。真正提供的是使用基类(指针或引用)来调用派生方法。您应该在此处展示如何使用 CRTP 完成它。vtablevtable
19赞 MikeMB 3/12/2015
在您的示例中,甚至没有调用,也没有在任何地方使用多态性。Base<>::method ()
1赞 Ivan Kush 9/18/2016
@Jichao,根据 @MikeMB 的注释,您应该调用 of 和 in 派生类名称,而不是methodImplmethodBasemethodImplmethod
1赞 barney 6/3/2017
如果您使用类似的 method(),那么它是静态绑定的,您不需要通用基类。因为无论如何,您都无法通过基类指针或 ref 多态使用它。因此,代码应如下所示: #include < iostream> template <typename T> struct Writer { void write() { static_cast<T*>(this)->writeImpl();struct Derived1 : public Writer<Derived1> { void writeImpl() { std::cout << “D1”;struct Derived2 : public Writer<Derived2> { void writeImpl() { std::cout << “DER2”;
64赞 GutiMac 11/4/2014 #3

在这里,你可以看到一个很好的例子。如果使用虚拟方法,程序将知道在运行时执行什么。实现 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);
    }
};

评论

1赞 atlex2 8/9/2016
你不能通过定义来做到这一点吗?虽然公平地说,这种技术在做其他工作时似乎非常有用。virtual void write(const char* str) const = 0;write
32赞 GutiMac 8/10/2016
使用纯虚方法,您可以在运行时而不是编译时解决继承问题。CRTP 用于在编译时解决这个问题,因此执行速度会更快。
3赞 2/11/2019
尝试创建一个需要抽象 Writer 的普通函数:你不能这样做,因为任何地方都没有名为 Writer 的类,那么你的多态性到底在哪里?这与虚拟功能完全不同,而且用处也小得多。
13赞 Mário Feroldi 11/27/2017 #4

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


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

[util.smartptr.enab]/1

类可以继承 to 继承成员函数,这些成员函数获取指向 的实例。Tenable_­shared_­from_­this<T>shared_­from_­thisshared_­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));
    }
};

你不能直接传递而不是传递的原因是它会破坏所有权机制:thisshared_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);
44赞 Chenna V 3/10/2018 #5

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

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

评论

2赞 wizurd 4/11/2018
在此示例中,添加一个示例,说明如何在 Base 类中实现默认的 foo(),如果没有 Derived 实现它,则将调用该示例。AKA 将 Base 中的 foo 更改为其他名称(例如 caller()),在 Base 中添加一个新函数 foo() 来选择“Base”。然后在 ProcessFoo 中调用 caller()
8赞 Pietro 9/12/2018
这是我最喜欢的答案,因为它也说明了为什么这种模式对函数很有用。ProcessFoo()
4赞 Gabriel Devillers 6/4/2020
我不明白这段代码的意义,因为无论有没有 Derived 和 AnotherDerived 实际派生,它仍然有效。恕我直言,如果 ProcessFoo 不以某种方式使用模板,那会更有趣。void ProcessFoo(T* b)
5赞 Chenna V 6/12/2020
@GabrielDevillers 首先,模板化将与实现接口的任何类型一起使用,即在这种情况下,输入类型 T 应该有一个名为 的方法。其次,为了让非模板化处理多种类型,你最终可能会使用 RTTI,这是我们想要避免的。此外,模板化版本为您提供了界面上的编译时间检查。ProcessFoo()foo()ProcessFoo
1赞 Paul 11/2/2021
非常感谢!这是最好的解释。简单的简短例子。与此类似的 en.cppreference.com/w/cpp/language/crtp
2赞 David Tsaturyan 1/28/2022 #6

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

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

template <typename T>
struct observer
{
    virtual void state_changed(T*, variant<string, int, bool>) = 0;
    virtual ~observer() {}
};

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

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)
    {

    }
};

它也是一个依赖于类型的参数类,它与传递给函数内部函数的对象相同。 剩下的只是介绍实际的主体类和观察者类。这里使用 CRTP 模式,我们从 observable<date>class date : public observable<date> 派生日期可观察类。T*state_changednotify_observersdatedate_drawer

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 并在发生状态更改时传递给通知函数(和此处)。允许我们在实际的观察者类中覆盖纯虚函数时使用,因此我们在它里面有 (不是 ),例如我们可以调用 (在我们的例子中) 的非虚函数 在函数中。 我们可以避免使用 CRTP,因此不参数化观察者设计模式实现,而是在任何地方使用基类指针。这样我们就可以达到同样的效果,但在这种情况下,每当我们想使用派生类指针时(即使不是很推荐),我们都应该使用下沉,它有一些运行时开销。thisnotify_observersset_date_propertiesset_datedate*void state_changed(date* c, variant<string, int, bool> state)date_drawerdate* cobservable*date*get_datestate_changedobservabledynamic_cast