c++ 什么是奇怪的循环模板模式(CRTP)?

5ssjco0h  于 11个月前  发布在  其他
关注(0)|答案(6)|浏览(111)

在没有参考书籍的情况下,任何人都可以用一个代码示例来很好地解释 CRTP(奇怪的重复模板模式)吗?

vsaztqbk

vsaztqbk1#

简而言之,CRTP是当类A有一个基类时,该基类是类A本身的模板特化。

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();


所以你明白了吗?单例模板假定它对任何类型X的专门化都将从singleton<X>继承,因此将具有其所有的(public,protected)成员可访问,包括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 <
}


或在模板范围内实现而不进行类型转换

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


如果你只为Apple写操作符==,这看起来会写得更少,但是想象一下,Equality模板不仅提供==,而且还提供>>=<=等。
CRTP是一个很棒的东西:)HTH

kpbwa7wx

kpbwa7wx2#

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

字符串

nimxete2

nimxete23#

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

fjaof16o

fjaof16o4#

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

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

[util.smartptr.enab]/1
T可以从enable_­shared_­from_­this<T>继承,以继承获得指向*thisshared_­ptr示例的shared_­from_­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);

68de4m5k

68de4m5k5#

正如注:
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

wmomyfyw

wmomyfyw6#

另一个使用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*,它将是状态被更改的对象。第二个参数将是被更改的字段,它可以是任何东西,甚至你可以省略它,这不是我们主题的问题(在这种情况下,它是3个字段的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)
    {

    }
};


这也是一个依赖于类型T*的参数类,并且这是传递给notify_observers函数内部的state_changed函数的同一个对象。仍然只是引入实际的主体类date和观察者类date_drawer这里使用了CRTP模式,我们从observable<date>导出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传递给notify notify_observers函数(这里是set_date_propertiesset_date)。允许我们在实际的date_drawer观察者类中重写void state_changed(date* c, variant<string, int, bool> state)纯虚函数时使用date*,因此,我们在其中有date* c(不是observable*),例如我们可以调用date*的非虚函数(get_date在我们的情况下)在state_changed函数内部。我们可以避免使用CRTP,因此不需要参数化观察者设计模式的实现和使用observable基类指针无处不在。这样我们可以有相同的效果,但在这种情况下,每当我们想使用派生类指针(即使不是很推荐),我们应该使用dynamic_cast向下转换,这会有一些运行时开销。

相关问题