221

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

4

6 回答 6

311

简而言之,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 还有其他有用的用途。例如,如果您想计算您的类当前存在的所有实例,但想将此逻辑封装在一个单独的模板中(具体类的想法非常简单 - 有一个静态变量,在 ctors 中递增,在 dtors 中递减)。尝试将其作为练习!

另一个有用的例子,对于 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! 
    {
    }
}

==如果你只写 operator for ,这看起来你会写得更少Apple,但想象一下Equality模板不仅会提供==but >>=<=。你可以将这些定义用于多个类,重用代码!

CRTP 是一个很棒的东西 :) HTH

于 2010-11-13T15:40:00.733 回答
56

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

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
于 2018-03-09T19:14:07.807 回答
11

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


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

[util.smartptr.enab]/1

一个类T可以继承自获得指向的实例enable_­shared_­from_­this<T>shared_­from_­this成员函数。shared_­ptr*this

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

当您需要提供 astd::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);
于 2017-11-27T15:26:43.357 回答
3

正如注释:

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
于 2013-10-11T06:13:56.693 回答
1

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

假设您有一个类,并且您有一些侦听器datedate_drawer,如以某种格式,提醒特定日期等)。你可以做的是有两个参数化的基类,你应该从中派生你的和观察者类(在我们的例子中)。对于观察者设计模式的实现,请参考GOF等经典书籍。这里我们只需要强调一下CRTP的使用。让我们看看它。在我们的草稿实现中,基类有一个纯虚方法,应该由date_reminderdateobserverobservabledatedate_drawerobserverobservable每当发生状态变化时,让我们调用这个方法state_changed。我们来看看这个小的抽象基类的代码。

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

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

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 模式,我们从:派生了dateobservable 类。observable<date>class date : public observable<date>

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给通知函数。允许我们在实际观察者类中覆盖纯虚函数时使用,因此我们在其中有 (not ),例如我们可以在函数内调用(在我们的例子中)的非虚函数。我们可以避免想要使用 CRTP,因此不会参数化观察者设计模式实现并在任何地方使用基类指针。这样我们可以得到相同的效果,但是在这种情况下,每当我们想要使用派生类指针(即使不是很推荐)我们都应该使用notify_observersset_date_propertiesset_datedate*void state_changed(date* c, variant<string, int, bool> state)date_drawerdate* cobservable*date*get_datestate_changedobservabledynamic_cast向下转换有一些运行时开销。

于 2022-01-28T13:49:33.617 回答