1. 程式人生 > 實用技巧 >《奇異遞迴模板模式(Curiously Recurring Template Pattern)》

《奇異遞迴模板模式(Curiously Recurring Template Pattern)》

weak_ptr 是關鍵:

吉良吉影 最近很忙,暫停更新...

本篇短文將簡短的介紹奇異遞迴模板模式(Curiously Recurring Template Pattern,CRTP),CRTP是C++模板程式設計時的一種慣用法(idiom):把派生類作為基類的模板引數。更一般地被稱作F-bound polymorphism。

1980年代作為F-bound polymorphism被提出。Jim Coplien於1995年稱之為CRTP。

CRTP在C++中主要有兩種用途:

  • 靜態多型(static polymorphism)
  • 新增方法同時精簡程式碼

1.靜態多型

先看一個簡單的例子:

#include <iostream>
using namespace std;

template <typename Child>
struct Base
{
	void interface()
	{
		static_cast<Child*>(this)->implementation();
	}
};

struct Derived : Base<Derived>
{
	void implementation()
	{
		cerr << "Derived implementation\n";
	}
};

int main()
{
	Derived d;
	d.interface();  // Prints "Derived implementation"

	return 0;
}

這裡基類Base為模板類,子類Drived繼承自Base同時模板引數為Drived,基類中有介面interface而子類中則有介面對應實現implementation,基類interface中將this通過static_cast轉換為模板引數型別,並呼叫該型別的implemention方法。由於Drived繼承基類時的模板為Drived型別所以在static_cast時會轉換為Drived並呼叫Drived的implemention方法。(注意這裡採用的時static_cast而不是dynamic_cast,因為只有繼承了Base的型別才能呼叫interface且這裡是向下轉型,所以採用static_cast是安全的。

)

通過CRTP可以使得類具有類似於虛擬函式的效果,同時又沒有虛擬函式呼叫時的開銷(虛擬函式呼叫需要通過虛擬函式指標查詢虛擬函式表進行呼叫),同時類的物件的體積相比使用虛擬函式也會減少(不需要儲存虛擬函式指標),但是缺點是無法動態繫結。

下面是關於靜態多型的第二個例子:

template<typename Child>
class Animal
{
public:
	void Run()
	{
		static_cast<Child*>(this)->Run();
	}
};

class Dog :public Animal<Dog>
{
public:
	void Run()
	{
		cout << "Dog Run" << endl;
	}
};

class Cat :public Animal<Cat>
{
public:
	void Run()
	{
		cout << "Cat Run" << endl;
	}
};

template<typename T>
void Action(Animal<T> &animal)
{
	animal.Run();
}

int main()
{
	Dog dog;
	Action(dog);

	Cat cat;
	Action(cat);
	return 0;
}

這裡Dog繼承自Animal且模板引數為Dog,Cat繼承自Animal且模板引數為Cat,Animal,Dog,Cat中都聲明瞭Run,而Animal中的Run是通過型別轉換後呼叫模板型別的Run方法實現的。在Action模板函式中接收Animal型別的引用(或指標)並在其中呼叫了animal物件的Run方法,由於這裡傳入的是不同的子類物件,因此Action中的animal也會有不同的行為。

2.新增方法,減少冗餘

假設現在我們需要實現一個數學運算庫,我們需要支援Vector2,Vector3,Vector4...等型別,如果我們將每個類分別宣告並實現如下:

//Vec3
struct Vector3
{
	float x;
	float y;
	float z;

	Vector3() = default;

	Vector3(float _x, float _y, float _z);

	inline Vector3& operator+=(const Vector3& rhs);
	inline Vector3& operator-=(const Vector3& rhs);
	//....
};

inline Vector3 operator+(const Vector3& lhs, const Vector3& rhs);
inline Vector3 operator-(const Vector3& lhs, const Vector3& rhs);
//....

//Vec2
struct Vector2
{
	float x;
	float y;

	Vector2() = default;

	Vector2(float _x, float _y);

	inline Vector2& operator+=(const Vector2& rhs);
	inline Vector2& operator-=(const Vector2& rhs);
	//....
};

inline Vector2 operator+(const Vector2& lhs, const Vector2& rhs);
inline Vector2 operator-(const Vector2& lhs, const Vector2& rhs);
//....

我們會發現需要為每個型別都實現+=, -= ,++ , -- , + , -等運算子過載,而且每個型別的一些運算子,行為都很類似,而且可以使用其他的運算子進行實現,比如+=, -=, ++, --都可以採用+,-運算子進行實現。這時我們就可以採用CRTP抽離出這些共同的類似方法,減少程式碼的冗餘:

template<typename T>
struct VectorBase
{
	T& underlying() { return static_cast<T&>(*this); }
	T const& underlying() const { return static_cast<T const&>(*this); }

	inline T& operator+=(const T& rhs) 
	{ 
		this->underlying() = this->underlying() + rhs;
		return this->underlying();
	}

	inline T& operator-=(const T& rhs)
	{
		this->underlying() = this->underlying() - rhs;
		return this->underlying();
	}
	
	//.....
};

struct Vector3 : public VectorBase<Vector3>
{
	float x;
	float y;
	float z;

	Vector3() = default;

	Vector3(float _x, float _y, float _z)
	{
		x = _x;
		y = _y;
		z = _z;
	}
};

inline Vector3 operator+(const Vector3& lhs, const Vector3& rhs)
{
	Vector3 result;
	result.x = lhs.x + rhs.x;
	result.y = lhs.y + rhs.y;
	result.z = lhs.z + rhs.z;
	return result;
}

inline Vector3 operator-(const Vector3& lhs, const Vector3& rhs)
{
	Vector3 result;
	result.x = lhs.x - rhs.x;
	result.y = lhs.y - rhs.y;
	result.z = lhs.z - rhs.z;
	return result;
}
//......

int main()
{
	Vector3 v0(6.0f, 5.0f, 4.0f);
	Vector3 v2(4.0f, 5.0f, 6.0f);

	v0 += v2;
	v0 -= v2;

	return 0;
}

通過把+=, -=等操作放到基類中並採用+ ,-運算子實現,這樣一來所有繼承自VectorBase的類,只要其定義了+,-運算子就可以自動獲得+=, -=等運算子,這樣大大的減少了程式碼中的冗餘。

在有多個型別存在相同方法,且這些方法可以藉助於類的其他方法進行實現時,均可以採用CRTP進行精簡程式碼。

參考:

1.The Curiously Recurring Template Pattern (CRTP)

2.The cost of dynamic (virtual calls) vs. static (CRTP) dispatch in C++

3.

PS:附加std::enable_shared_from_this相關內容

假如在c++中想要在一個已被shareptr管理的型別物件內獲取並返回this,為了防止被管理的物件已被智慧指標釋放,而導致this成為懸空指標,可能會考慮以share_ptr的形式返回this指標,程式碼實現如下:

struct Bad
{
    // 錯誤寫法:用不安全的表示式試圖獲得 this 的 shared_ptr 物件
    std::shared_ptr<Bad> getptr() {
        return std::shared_ptr<Bad>(this);
    }
    ~Bad() { std::cout << "Bad::~Bad() called\n"; }
};

int main()
{
	{
		// 錯誤的示例,每個 shared_ptr 都認為自己是物件僅有的所有者
		std::shared_ptr<Bad> bp1 = std::make_shared<Bad>();
		std::shared_ptr<Bad> bp2 = bp1->getptr();
		std::cout << "bp2.use_count() = " << bp2.use_count() << '\n';
	}

	return 0;
}

但是上面的寫法是完全錯誤的,因為share_ptr內部儲存了兩個指標,一個指向被管理物件,另一個指向控制塊。控制塊記憶體有刪除器,佔有被管理物件的shared_ptr的數量,涉及被管理物件的weak_ptr的數量等資訊,一旦佔有被管理物件的shared_ptr的數量減少至0,被管理的物件就會通過刪除器被釋放(控制塊會等到weakptr計數器也清0時才會釋放)。上面的程式碼中由於在返回this的sharedptr時,又通過this指標構造了一個shared_ptr,這樣就會導致有兩個shared_ptr通過不同的控制塊,管理相同的物件。一旦其中一個shared_ptr釋放了所管理的物件,那麼另一個shared_ptr將會變成非法的。

而正確的寫法應該是讓需要返回this指標的類,繼承std::enable_shared_from_this模板類,同時模板引數為該類的型別:

struct Good: std::enable_shared_from_this<Good> // 注意:繼承
{
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

至於為什麼要這樣做,可以參見以下的虛擬碼:

template<class D>
class enable_shared_from_this {
protected:
    constexpr enable_shared_from_this() { }
    enable_shared_from_this(enable_shared_from_this const&) { }
    enable_shared_from_this& operator=(enable_shared_from_this const&) {
        return *this;
    }

public:
    shared_ptr<T> shared_from_this() { return self_; }
    shared_ptr<T const> shared_from_this() const { return self_; }

private:
    weak_ptr<D> self_;

    friend shared_ptr<D>;
};

template<typename T>
shared_ptr<T>::shared_ptr(T* ptr) {
    // ...
    // Code that creates control block goes here.
    // ...

    // NOTE: This if check is pseudo-code. Won't compile. There's a few
    // issues not being taken in to account that would make this example
    // rather noisy.
    if (is_base_of<enable_shared_from_this<T>, T>::value) {
        enable_shared_from_this<T>& base = *ptr;
        base.self_ = *this;
    }
}

enable_shared_from_this類使用了CRTP的寫法,類中儲存了一個weak_ptr<T>,使用這個weak_ptr<t>初始化shared_ptr<T>時,如果這個型別是否繼承自enable_shared_from_this,則會使shared_from_this中構造的shared_ptr<T>共享weak_ptr<T>指向的物件的所有權(即從weak_ptr構造的shared_ptr和構造weak_ptr的shared_ptr共享控制塊)(std::shared_ptr::shared_ptr - cppreference.com參見建構函式11),這樣就可以保證shared_from_this返回的shared_ptr的記憶體安全,不會像第一個例子那樣出現懸空指標。

(enable_shared_from_this中的weak_ptr是通過shared_ptr的建構函式初始化的,所以必須在shared_ptr建構函式呼叫之後才能呼叫shared_from_this,而且不能對一個沒有被shared_ptr接管的類呼叫shared_from_this,否則會產生未定義行為)

以上記憶體參考自:

  1. std::shared_ptr - cppreference.com
  2. How std::enable_shared_from_this::shared_from_this works
  3. enable_shared_from_this類的作用和實現 - 楊文的部落格 - 部落格園
  4. 關於boost中enable_shared_from_this類的原理分析 - 阿瑪尼迪迪 - 部落格園
編輯於 2019-01-24
https://en.cppreference.com/w/cpp/memory/enable_shared_from_this
 1 #include <memory>
 2 #include <iostream>
 3  
 4 struct Good: std::enable_shared_from_this<Good> // note: public inheritance
 5 {
 6     std::shared_ptr<Good> getptr() {
 7         return shared_from_this();
 8     }
 9 };
10  
11 struct Bad
12 {
13     std::shared_ptr<Bad> getptr() {
14         return std::shared_ptr<Bad>(this);
15     }
16     ~Bad() { std::cout << "Bad::~Bad() called\n"; }
17 };
18  
19 int main()
20 {
21     // Good: the two shared_ptr's share the same object
22     std::shared_ptr<Good> gp1 = std::make_shared<Good>();
23     std::shared_ptr<Good> gp2 = gp1->getptr();
24     std::cout << "gp2.use_count() = " << gp2.use_count() << '\n';
25  
26     // Bad: shared_from_this is called without having std::shared_ptr owning the caller 
27     try {
28         Good not_so_good;
29         std::shared_ptr<Good> gp1 = not_so_good.getptr();
30     } catch(std::bad_weak_ptr& e) {
31         // undefined behavior (until C++17) and std::bad_weak_ptr thrown (since C++17)
32         std::cout << e.what() << '\n';    
33     }
34  
35     // Bad, each shared_ptr thinks it's the only owner of the object
36     std::shared_ptr<Bad> bp1 = std::make_shared<Bad>();
37     std::shared_ptr<Bad> bp2 = bp1->getptr();
38     std::cout << "bp2.use_count() = " << bp2.use_count() << '\n';
39 } // UB: double-delete of Bad

1 gp2.use_count() = 2
2 bad_weak_ptr
3 bp2.use_count() = 1
4 Bad::~Bad() called
5 demo(45162,0x1131cb5c0) malloc: *** error for object 0x7f8be9400658: pointer being freed was not allocated
6 demo(45162,0x1131cb5c0) malloc: *** set a breakpoint in malloc_error_break to debug
7 
8 Process finished with exit code 6