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

5ssjco0h  于 2024-01-09  发布在  其他
关注(0)|答案(6)|浏览(154)

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

vsaztqbk

vsaztqbk1#

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

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

字符串
这是一个奇怪的循环,不是吗?:)
现在,这给了你什么给予呢?这实际上给了X模板成为其专门化的基类的能力。
例如,您可以像这样创建一个通用的单例类(简化版)

  1. #include <iostream>
  2. template <class T>
  3. class Singleton
  4. {
  5. public:
  6. static T* GetInstance() {
  7. if ( p == nullptr ) p = new T();
  8. return p;
  9. }
  10. protected:
  11. Singleton() = default;
  12. Singleton(Singleton const &) = delete;
  13. Singleton &operator=(const Singleton &) = delete;
  14. private:
  15. static T *p;
  16. };
  17. template <class T>
  18. T *Singleton<T>::p= nullptr;


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

  1. class A : public Singleton<A>
  2. {
  3. friend Singleton;
  4. private:
  5. A() = default;
  6. };
  7. A *a0= A::GetInstance();


然而,在这种情况下,CRTP是不必要的,见以下:

  1. class C
  2. {
  3. friend Singleton<C>;
  4. private: C() = default;
  5. };
  6. C *c1= Singleton<C>::GetInstance();


所以你明白了吗?单例模板假定它对任何类型X的专门化都将从singleton<X>继承,因此将具有其所有的(public,protected)成员可访问,包括GetInstance!CRTP还有其他有用的用途。例如,如果你想计算你的类当前存在的所有示例,但是想把这个逻辑封装在一个单独的模板中(具体类的想法很简单--有一个静态变量,在ctors中递增,在dtors中递减)。
另一个有用的例子,对于Boost(我不确定他们是如何实现的,但CRTP也会这样做)。想象一下,你只想为你的类提供运算符<,但自动为它们提供运算符==
你可以这样做:

  1. template<class Derived>
  2. class Equality
  3. {
  4. };
  5. template <class Derived>
  6. bool operator == (Equality<Derived> const& op1, Equality<Derived> const & op2)
  7. {
  8. Derived const& d1 = static_cast<Derived const&>(op1);//you assume this works
  9. //because you know that the dynamic type will actually be your template parameter.
  10. //wonderful, isn't it?
  11. Derived const& d2 = static_cast<Derived const&>(op2);
  12. return !(d1 < d2) && !(d2 < d1);//assuming derived has operator <
  13. }


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

  1. template<class T>
  2. class Equality
  3. {
  4. friend bool operator == (const T& op1, const T& op2)
  5. {
  6. return !(op1 < op2) && !(op2 < op1);
  7. }
  8. };


现在你可以像这样使用它

  1. struct Apple:public Equality<Apple>
  2. {
  3. int size;
  4. };
  5. bool operator < (Apple const & a1, Apple const& a2)
  6. {
  7. return a1.size < a2.size;
  8. }


现在,您还没有为Apple显式地提供运算符==?但是您已经有了!您可以这样写:

  1. int main()
  2. {
  3. Apple a1;
  4. Apple a2;
  5. a1.size = 10;
  6. a2.size = 10;
  7. if(a1 == a2) //the compiler won't complain!
  8. {
  9. }
  10. }


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

展开查看全部
kpbwa7wx

kpbwa7wx2#

在这里你可以看到一个很棒的例子。如果你使用虚方法,程序将知道在运行时执行什么。实现CRTP,编译器在编译时决定哪个!这是一个很棒的性能!

  1. template <class T>
  2. class Writer
  3. {
  4. public:
  5. Writer() { }
  6. ~Writer() { }
  7. void write(const char* str) const
  8. {
  9. static_cast<const T*>(this)->writeImpl(str); //here the magic is!!!
  10. }
  11. };
  12. class FileWriter : public Writer<FileWriter>
  13. {
  14. public:
  15. FileWriter(FILE* aFile) { mFile = aFile; }
  16. ~FileWriter() { fclose(mFile); }
  17. //here comes the implementation of the write method on the subclass
  18. void writeImpl(const char* str) const
  19. {
  20. fprintf(mFile, "%s\n", str);
  21. }
  22. private:
  23. FILE* mFile;
  24. };
  25. class ConsoleWriter : public Writer<ConsoleWriter>
  26. {
  27. public:
  28. ConsoleWriter() { }
  29. ~ConsoleWriter() { }
  30. void writeImpl(const char* str) const
  31. {
  32. printf("%s\n", str);
  33. }
  34. };

字符串

展开查看全部
nimxete2

nimxete23#

CRTP是一种实现编译时多态性的技术。这里有一个非常简单的例子。在下面的例子中,ProcessFoo()使用Base类接口,Base::Foo调用派生对象的foo()方法,这就是你想用虚方法做的事情。
http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e

  1. template <typename T>
  2. struct Base {
  3. void foo() {
  4. (static_cast<T*>(this))->foo();
  5. }
  6. };
  7. struct Derived : public Base<Derived> {
  8. void foo() {
  9. cout << "derived foo" << endl;
  10. }
  11. };
  12. struct AnotherDerived : public Base<AnotherDerived> {
  13. void foo() {
  14. cout << "AnotherDerived foo" << endl;
  15. }
  16. };
  17. template<typename T>
  18. void ProcessFoo(Base<T>* b) {
  19. b->foo();
  20. }
  21. int main()
  22. {
  23. Derived d1;
  24. AnotherDerived d2;
  25. ProcessFoo(&d1);
  26. ProcessFoo(&d2);
  27. return 0;
  28. }

字符串
输出量:

  1. derived foo
  2. 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时,它很有用:

  1. struct Node;
  2. void process_node(const std::shared_ptr<Node> &);
  3. struct Node : std::enable_shared_from_this<Node> // CRTP
  4. {
  5. std::weak_ptr<Node> parent;
  6. std::vector<std::shared_ptr<Node>> children;
  7. void add_child(std::shared_ptr<Node> child)
  8. {
  9. process_node(shared_from_this()); // Shouldn't pass `this` directly.
  10. child->parent = weak_from_this(); // Ditto.
  11. children.push_back(std::move(child));
  12. }
  13. };

字符串
不能直接传递this而不是shared_from_this()的原因是它会破坏所有权机制:

  1. struct S
  2. {
  3. std::shared_ptr<S> get_shared() const { return std::shared_ptr<S>(this); }
  4. };
  5. // Both shared_ptr think they're the only owner of S.
  6. // This invokes UB (double-free).
  7. std::shared_ptr<S> s1 = std::make_shared<S>();
  8. std::shared_ptr<S> s2 = s1->get_shared();
  9. assert(s2.use_count() == 1);

展开查看全部
68de4m5k

68de4m5k5#

正如注:
CRTP可以用来实现静态多态(类似于动态多态,但没有虚函数指针表)。

  1. #pragma once
  2. #include <iostream>
  3. template <typename T>
  4. class Base
  5. {
  6. public:
  7. void method() {
  8. static_cast<T*>(this)->method();
  9. }
  10. };
  11. class Derived1 : public Base<Derived1>
  12. {
  13. public:
  14. void method() {
  15. std::cout << "Derived1 method" << std::endl;
  16. }
  17. };
  18. class Derived2 : public Base<Derived2>
  19. {
  20. public:
  21. void method() {
  22. std::cout << "Derived2 method" << std::endl;
  23. }
  24. };
  25. #include "crtp.h"
  26. int main()
  27. {
  28. Derived1 d1;
  29. Derived2 d2;
  30. d1.method();
  31. d2.method();
  32. return 0;
  33. }

字符串
输出将是:

  1. Derived1 method
  2. Derived2 method

展开查看全部
wmomyfyw

wmomyfyw6#

另一个使用CRTP的好例子是观察者设计模式的实现。
假设你有一个类date,你有一些侦听器类,如date_drawerdate_reminder等。(观察员)应由主题类date通知(可观察到的)每当日期更改完成,以便他们可以做他们的工作(以某种格式绘制日期,提醒特定日期,你可以做的是有两个参数化的基类observerobservable,你应该从它们派生出你的date和观察者类(在我们的例子中是date_drawer)。对于观察者设计模式的实现,请参考像GOF这样的经典书籍。这里我们只需要强调CRTP的使用。让我们来看看它。在我们的草案实现中,observer基类有一个纯虚方法,每当状态发生变化时,observable类都应该调用它,让我们把这个方法命名为state_changed。让我们看看这个小的抽象基类的代码。

  1. template <typename T>
  2. struct observer
  3. {
  4. virtual void state_changed(T*, variant<string, int, bool>) = 0;
  5. virtual ~observer() {}
  6. };

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

  1. template <typename T>
  2. class observable
  3. {
  4. vector<unique_ptr<observer<T>>> observers;
  5. protected:
  6. void notify_observers(T* changed_obj, variant<string, int, bool> changed_state)
  7. {
  8. for (unique_ptr<observer<T>>& o : observers)
  9. {
  10. o->state_changed(changed_obj, changed_state);
  11. }
  12. }
  13. public:
  14. void subscribe_observer(unique_ptr<observer<T>> o)
  15. {
  16. observers.push_back(move(o));
  17. }
  18. void unsubscribe_observer(unique_ptr<observer<T>> o)
  19. {
  20. }
  21. };


这也是一个依赖于类型T*的参数类,并且这是传递给notify_observers函数内部的state_changed函数的同一个对象。仍然只是引入实际的主体类date和观察者类date_drawer这里使用了CRTP模式,我们从observable<date>导出date可观测类:class date : public observable<date>

  1. class date : public observable<date>
  2. {
  3. string date_;
  4. int code;
  5. bool is_bank_holiday;
  6. public:
  7. void set_date_properties(int code_ = 0, bool is_bank_holiday_ = false)
  8. {
  9. code = code_;
  10. is_bank_holiday = is_bank_holiday_;
  11. //...
  12. notify_observers(this, code);
  13. notify_observers(this, is_bank_holiday);
  14. }
  15. void set_date(const string& new_date, int code_ = 0, bool is_bank_holiday_ = false)
  16. {
  17. date_ = new_date;
  18. //...
  19. notify_observers(this, new_date);
  20. }
  21. string get_date() const { return date_; }
  22. };
  23. class date_drawer : public observer<date>
  24. {
  25. public:
  26. void state_changed(date* c, variant<string, int, bool> state) override
  27. {
  28. visit([c](const auto& x) {cout << "date_drawer notified, new state is " << x << ", new date is " << c->get_date() << endl; }, state);
  29. }
  30. };


让我们写一些客户端代码:

  1. date c;
  2. c.subscribe_observer(make_unique<date_drawer>());
  3. c.set_date("27.01.2022");
  4. c.set_date_properties(7, true);


该测试程序的输出将是。

  1. date_drawer notified, new state is 27.01.2022, new date is 27.01.2022
  2. date_drawer notified, new state is 7, new date is 27.01.2022
  3. 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向下转换,这会有一些运行时开销。

展开查看全部

相关问题