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


187

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


2
阅读SO上的CRTP问题:stackoverflow.com/questions/tagged/crtp。那可能会给你一些想法。
2010年

68
@sbi:如果他这样做,他会找到自己的问题。而且那会奇怪地反复发生。:)
Craig McQueen 2013年

1
顺便说一句,在我看来,该术语应该是“好奇地递归”。我误会了意思吗?
Craig McQueen 2013年

1
克雷格:我想你是。从某种意义上说,它是在多种情况下突然出现的,因此它“反复出现”。
Gareth McCaughan

Answers:


275

简而言之,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<X>,从而可以访问其所有(公共,受保护)成员,包括GetInstance!CRTP还有其他有用的用途。例如,如果您要计算您的类当前存在的所有实例,但想将此逻辑封装在单独的模板中(具体类的想法很简单-拥有一个静态变量,ctor中的增量,dtor中的递减)。尝试做为练习!

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

现在您可以像这样使用它

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模板将不仅提供==,但是>>=<=等你可以使用这些定义为多个类,重用代码!

CRTP是一件了不起的事情:) HTH


61
这篇文章并不主张将singleton作为良好的编程模式。它只是将其用作可以理解的
示例。imothe

3
@Armen:答案以一种易于理解的方式解释了CRTP,这是一个很好的答案,谢谢您提供如此好的答案。
Alok保存

1
@Armen:感谢您的出色解释。我以前还不太喜欢CRTP,但是平等的例子一直很启发人!+1
保罗

1
使用CRTP的另一个示例是当您需要不可复制的类时:template <class T> class NonCopyable {protected:NonCopyable(){}〜NonCopyable(){} private:NonCopyable(const NonCopyable&); NonCopyable&operator =(const NonCopyable&); }; 然后,您将使用不可复制,如下所示:类互斥体:私有NonCopyable <Mutex> {公共:void Lock(){} void UnLock(){}};
维伦2014年

2
@Puppy:Singleton并不可怕。当其他方法更合适时,它通常会被低于平均水平的程序员过度使用,但是大多数用法是可怕的,但这并不会使模式本身变得可怕。在某些情况下,单例是最佳选择,尽管这种情况很少见。
Kaiserludi

47

在这里您可以看到一个很好的例子。如果使用虚拟方法,程序将知道在运行时执行什么。实现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在进行其他工作时似乎很有帮助。
atlex2'8

25
使用纯虚拟方法可以在运行时而不是编译时解决继承问题。CRTP用于在编译时解决此问题,因此执行速度更快。
GutiMac'8

1
尝试制作一个需要抽象Writer的普通函数:您不能这样做,因为任何地方都没有名为Writer的类,那么您的多态性到底在哪里?这根本不等同于虚函数,它的用处不大。

22

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

1
在此示例中,可能还值得添加一个示例,该示例如何在Base类中实现默认的foo(),如果没有Derived实现,则将调用该默认foo()。也就是将Base中的foo更改为其他名称(例如caller()),然后将cout的“ Base”添加到Base中的新函数foo()。然后在ProcessFoo内部调用caller()
wizurd 18'Apr

@wizurd此示例更多地说明了纯虚拟基类函数,即我们强制foo()执行由派生类实现的函数。
blueskin

3
这是我最喜欢的答案,因为它也显示了此模式对ProcessFoo()函数有用的原因。
Pietro

我不明白这段代码的意义,因为void ProcessFoo(T* b)无论是否有Derived和AnotherDerived实际派生,它仍然可以工作。恕我直言,如果ProcessFoo不以某种方式使用模板,将会更有趣。
加百利·德维勒

1
@GabrielDevillers首先,模板化ProcessFoo()将与实现接口的任何类型一起使用,即在这种情况下,输入类型T应该具有称为的方法foo()。其次,为了使非模板化ProcessFoo可以使用多种类型,您可能最终会使用RTTI,这是我们想要避免的。此外,模板化版本为您提供了在界面上检查编译时间的功能。
blueskin

6

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


的一个很好的具体的例子CRTPstd::enable_shared_from_this从C ++ 11:

[util.smartptr.enab] / 1

T可以继承自来enable_­shared_­from_­this<T>继承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

1
不好意思,static_cast负责更改。如果您仍然想看到拐角处的情况,即使它不会引起错误,请参见此处:ideone.com/LPkktf
odinthenerd

30
不好的例子。如果vtable不使用CRTP,则可以使用no来完成此代码。什么vtableš真正提供使用基类(指针或引用)来调用导出方法是。您应该在此处显示如何使用CRTP进行操作。
Etherealone

17
在您的示例中,Base<>::method ()甚至没有调用,也不在任何地方使用多态。
MikeMB

1
@Jichao,根据@MikeMB的笔记,你应该叫methodImplmethodBase和在派生类的名字methodImpl,而不是method
伊万库什

1
如果使用类似的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”; };
barney
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.