【问题标题】:What is the curiously recurring template pattern (CRTP)?什么是奇怪的重复模板模式(CRTP)?
【发布时间】:2019-11-18 06:15:09
【问题描述】:

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

【问题讨论】:

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

标签: c++ templates c++-faq crtp


【解决方案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 是一种实现编译时多态性的技术。这是一个非常简单的例子。在下面的示例中,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()
  • @wizurd 这个例子更多是为了说明一个纯虚拟基类函数,即我们强制foo()由派生类实现。
  • 这是我最喜欢的答案,因为它也说明了为什么这种模式对ProcessFoo() 函数很有用。
  • 我不明白这段代码的意义,因为使用 void ProcessFoo(T* b) 并且没有 Derived 和 AnotherDerived 实际上派生它仍然可以工作。恕我直言,如果 ProcessFoo 不以某种方式使用模板会更有趣。
  • @GabrielDevillers 首先,模板化的ProcessFoo() 将适用于实现接口的任何类型,即在这种情况下,输入类型T 应该有一个名为foo() 的方法。其次,为了让非模板化的ProcessFoo 能够处理多种类型,您最终可能会使用我们想要避免的 RTTI。此外,模板化版本为您提供了界面上的编译时间检查。
【解决方案3】:

这不是一个直接的答案,而是一个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);

【讨论】:

    【解决方案4】:

    在这里你可以看到一个很好的例子。如果您使用虚拟方法,程序将知道在运行时执行什么。实现 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 的类,那么你的多态性到底在哪里?这根本不等同于虚函数,而且它的用处要小得多。
    【解决方案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