POD 型別

TriviallyCopyable:如果 T 是此類型,則可以使用 memcpy 高效複製,使用 std::is_trivially_copyable 來判斷是否為此類型。

TrivialType:TriviallyCopyable 且有一個或多個 default 建構子,所有這些建構子都是 trivial 或 deleted,並且至少有一個是非 deleted。使用 std::is_trivial 來判斷是否為此類型。

StandardLayoutType:如果 T 是此類型,則相容於 C,可以和 C 程式互相操作,使用 std::is_standard_layout 來判斷是否為此類型。

PODType:TriviallyCopyable 且 StandardLayoutType。使用 std::is_pod 來判斷是否為此類型。

當我們設計看盤軟體的資料結構時,會希望滿足 POD 型別,這樣可以高效的複製並且可以最大相容於 C API。

#include <type_traits>
struct K
{
	unsigned long d; // 日期
	unsigned long t; // 時間
	double o; // 開
	double h; // 高
	double l; // 低
	double c; // 收
	double v; // 量
};
int main()
{
	std::cout << std::boolalpha;
	std::cout << std::is_trivially_copyable<K>::value << std::endl;
	std::cout << std::is_trivial<K>::value << std::endl;
	std::cout << std::is_standard_layout<K>::value << std::endl;
	std::cout << std::is_pod<K>::value << std::endl;
	return 0;
}
POD type

設計資料結構節省記憶體的小撇步

以下是三種資料結構,X1 的成員變數亂亂排,X2 由小到大排列,X3 由大到小排列,猜猜看這三種資料結構的大小是多少?

struct X1
{
	char a;     // 1 byte
	int b;      // 4 bytes
	short c;    // 2 bytes
	char d;     // 1 byte
};
struct X2
{
	char a;     // 1 byte
	char d;     // 1 byte
	short c;    // 2 bytes
	int b;      // 4 bytes
};
struct X3
{
	int b;      // 4 bytes
	short c;    // 2 bytes
	char a;     // 1 byte
	char d;     // 1 byte
};

答案如下,有出乎意料之外嗎?

data alignment

這是因為資料對齊的關係,資料對齊則又跟 CPU 存取效能有關,在 X1 結構中是以最大的資料成員,也就是 int (4 bytes) 為對齊方式,因此編譯器會以自然對齊的方式來對齊結構成員去填補結構,如下:

struct X1
{
	char a;		// 1 byte
	char pad0[3];	// 填補對齊到 4 位元組的邊界
	int b;		// 4 bytes
	short c;	// 2 bytes
	char d;		// 1 byte
	char pad1[1];	// 填補對齊到 4 的倍數
};

而結構 X2 及 X3 則有效利用了空間,因此記憶體用量也比較小。

那為什麼要講這個呢?因為在設計看盤軟體的 K 棒資料結構時,如果亂排可是會浪費很多的記憶體的,眾所周知股價資料是很大的,一天可以高達好幾十 GB,因此有必要節省記憶體的用量,以下這兩種設計記憶體用量就差距很大:

struct K1
{
	unsigned long d; // 日期
	double o; // 開
	double h; // 高
	double l; // 低
	double c; // 收
	double v; // 量
	unsigned long t; // 時間
};
struct K2
{
	unsigned long d; // 日期
	unsigned long t; // 時間
	double o; // 開
	double h; // 高
	double l; // 低
	double c; // 收
	double v; // 量
};
不同的 K 棒資料結構大小差距

因此可以的話還是好好排列一下成員變數吧!

如何使用 C++ 產生 UUID / GUID

寫程式時常常會需要用到 UUID 來當作唯一的鍵值。例如觀察者模式中,觀察者訂閱主題時,主題類別可以回傳一個 UUID 給觀察者保存,觀察者用此 UUID 取消訂閱,可以解決有些觀察者類別不能比較,因此無法用來搜尋並取消訂閱的問題。

以下簡單介紹使用 boost.uuid 函式庫:

#include <boost/uuid/uuid.hpp> // 為了宣告 uuid 型別
#include <boost/uuid/random_generator.hpp> // 為了產生 uuid
boost::uuids::uuid id = boost::uuids::random_generator()();

如果對 boost::uuids::random_generator()() 這行 code 感到陌生,這其實是呼叫 boost::uuids::random_generator() 建構實體,再呼叫成員函式 operator() 的意思,等同下面兩行 code:

boost::uuids::random_generator g;
boost::uuids::uuid id = g();

boost::uuids::uuid 是一個 POD 型別,內含成員變數 uint8_t data[16],設計成 POD 型別表示可以使用 memset 或 memcpy 等函式,效率好。

也可以將 boost::uuids::uuid 轉換成 std::string:

#include <boost/uuid/uuid_io.hpp> // to_string
boost::uuids::uuid id = boost::uuids::random_generator()();
auto s = boost::uuids::to_string(id);

下圖為產生的 UUID:

UUID

最後要提醒的一點是,boost.uuid 廢棄了 name_generator 的使用,因為 MD5SHA1 已經被攻破。

升級現代 C++ 該改變的習慣

1. 使用 nullptr,而不要使用 NULL 或 0 表示空指標。

考慮以下兩個 Func,傳入 0 時會呼叫 Func(int),而傳入 NULL 時會預期呼叫的是 Func(void*),可惜事與願違,呼叫的還是 Func(int),這是因為 NULL 被定義為 0,在 C++ 11 你只要改用 nullptr 就可以消除這種歧義。

void Func(void*)
{}
void Func(int)
{}
int main()
{
	Func(0); // call Func(int)
	Func(NULL); // call Func(int),不如預期
	Func(nullptr); // call Func(void*)
	return 0;
}

2. 使用 Raw string literals 取代逸出字元的使用。

以前要顯示 ” 或 \ 之類的字元需要在前面加上 \,現在不用了,直接將字串用 R”(要顯示的字串)” 包起來即可。

int main()
{
	const char* s1 = "\"hello\nworld\"";
	const char* s2 = R"(\"hello\nworld\")";
	const char* s3 = R"("hello
world")";
	std::cout << s1 << std::endl;
	std::cout << std::endl;
	std::cout << s2 << std::endl;
	std::cout << std::endl;
	std::cout << s3 << std::endl;
	return 0;
}
Raw string literals

3. 使用 Digit separators 增加程式碼可讀性。

如果程式碼內有一個很大的數字,現在可以使用 ‘ 來幫助閱讀,例如當作千分位使用。

int main()
{
	unsigned long long n1 = 10929085235;
	unsigned long long n2 = 10'929'085'235;
	std::cout << n1 << std::endl;
	std::cout << n2 << std::endl;
	return 0;
}
Digit separators

使用 final 明確表達不想被繼承或改寫的意圖

如果你設計了一個類別,它不適合被繼承使用,例如它沒有虛擬解構子 (virtual destructor)、沒有虛擬函式 (virtual function),也沒有 protected 函式,那麼請為它加上 final,這樣編譯器便可以阻止此類別被繼承。

class CString final
{
	// ...
};

接下來考慮一種情況,如果有一個交易系統要寫 log,它使用通用的 log 類別,所有的函式都滿足需求,只有一個函式 (SendLog) 的功能不滿足,但好消息是此函式是虛擬函式可以被改寫,如下:

class CLog
{
public:
	virtual ~CLog()
	{}
public:
	virtual void SendLog()
	{}
};

因為兩者間不是 is-a 關係,而是 is-implemented-in-terms-of (根據某物實作出) 關係,因此我們選擇 private 繼承 CLog,並改寫 SendLog。

class CTradingSystem : private CLog
{
public:
	virtual ~CTradingSystem()
	{}
private:
	virtual void SendLog() // 改寫 SendLog
	{}
};

但我們可能希望 CTradingSystem 也可以被當作基底類別,又不想繼承的類別去改寫 SendLog,為了隱藏 SendLog,在 C++ 03 前我們可能會使用嵌套式的私有類別,並且由此類別去 public 繼承 CLog 並改寫 SendLog,如下:

class CTradingSystem
{
public:
	virtual ~CTradingSystem()
	{}
private:
	class CTradingSystemLog : public CLog
	{
	public:
		virtual ~CTradingSystemLog()
		{}
	public:
		virtual void SendLog() // 改寫 SendLog
		{}
	};
private:
	CTradingSystemLog m_log; // 這樣來使用 SendLog
};

好消息是,在 C++ 11 不用那麼麻煩了,你可以直接為不想被改寫的虛擬函式加上 final 解決這件事。

class CTradingSystem : private CLog
{
public:
	virtual ~CTradingSystem()
	{}
private:
	virtual void SendLog() final // 阻止繼承的類別改寫
	{}
};

請將繼承來的虛擬函式都加上 override

如果想要成功覆寫 (override) 基底類別的虛擬函式 (virtual function),你應該知道要滿足一些條件:

  • 基底類別的函式要宣告為虛擬函式 (#1)。
  • 基底類別和衍生類別的函式名稱要相同 (#2)。
  • 基底類別和衍生類別的函式參數型別及個數要相同 (#3, #4)。
  • 基底類別和衍生類別的函式 const 或 volatile 宣告要相同 (#5, #6)。
  • 基底類別和衍生類別的函式回傳型態要相同 (#7)。
  • 基底類別和衍生類別的函式參考限定符要相同 (#8)。
  • 基底類別和衍生類別的函式例外規格要相同 (#9)。

假設有一個基底類別 (base class),如下:

class Base
{
public:
	virtual ~Base()
	{}
public:
	void NonVirtual() // #1
	{
		std::cout << "Base::NonVirtual" << std::endl;
	}
	virtual void DiffFuncName() // #2
	{
		std::cout << "Base::DiffFuncName" << std::endl;
	}
	virtual void DiffParamType(int n) // #3
	{
		std::cout << "Base::DiffParamType" << std::endl;
	}
	virtual void DiffParamNum(int n1, int n2) // #4
	{
		std::cout << "Base::DiffParamNum" << std::endl;
	}
	virtual void DiffConstState() const // #5
	{
		std::cout << "Base::DiffConstState" << std::endl;
	}
	virtual void DiffVolatileState() volatile // #6
	{
		std::cout << "Base::DiffVolatileState" << std::endl;
	}
	virtual double DiffReturnType() // #7
	{
		std::cout << "Base::DiffReturnType" << std::endl;
		return 0;
	}
	virtual void DiffRefType() & // #8
	{
		std::cout << "Base::DiffRefType" << std::endl;
	}
	virtual void DiffExceptionSpec() noexcept // #9
	{
		std::cout << "Base::DiffExceptionSpec" << std::endl;
	}
};

衍生類別 (derived class),如下:

class Derived : public Base
{
public:
	virtual ~Derived()
	{}
public:
	void NonVirtual() // #1
	{
		std::cout << "Derived::NonVirtual" << std::endl;
	}
	virtual void DiffFuncName1() // #2
	{
		std::cout << "Derived::DiffFuncName1" << std::endl;
	}
	virtual void DiffParamType(unsigned int n) // #3
	{
		std::cout << "Derived::DiffParamType" << std::endl;
	}
	virtual void DiffParamNum(int n1, int n2, int n3) // #4
	{
		std::cout << "Derived::DiffParamNum" << std::endl;
	}
	virtual void DiffConstState() // #5
	{
		std::cout << "Derived::DiffConstState" << std::endl;
	}
	virtual void DiffVolatileState() // #6
	{
		std::cout << "Derived::DiffVolatileState" << std::endl;
	}
	virtual float DiffReturnType() // #7
	{
		std::cout << "Derived::DiffReturnType" << std::endl;
		return 0;
	}
	virtual void DiffRefType() && // #8
	{
		std::cout << "Derived::DiffRefType" << std::endl;
	}
	virtual void DiffExceptionSpec() // #9
	{
		std::cout << "Derived::DiffExceptionSpec" << std::endl;
	}
};

以上這九種 case 都不能成功被 override,在筆者使用的 vs2019 也只報錯函式回傳型態不同 (#7) 及函式例外規格不同 (#9) 這兩種 case。

P.S. 如果 noexcept 是宣告在衍生類別,而基底類別沒有宣告,則因為是從寬鬆的例外規格變成嚴格的例外規格,可以成功覆寫。

not use override

可以看到拿掉無法編譯的 #7 及 #9,程式執行後全部都呼叫到基底類別的函式。

override failure

但如果衍生類別繼承來的虛擬函式都加上 override,則編譯器都會報錯。

class Derived : public Base
{
public:
	void NonVirtual() override; // #1
	virtual void DiffFuncName1() override;  // #2
	virtual void DiffParamType(unsigned int n) override; // #3
	virtual void DiffParamNum(int n1, int n2, int n3) override; // #4
	virtual void DiffConstState() override; // #5
	virtual void DiffVolatileState() override; // #6
	virtual float DiffReturnType() override; // #7
	virtual void DiffRefType() && override; // #8
	virtual void DiffExceptionSpec() override; // #9
};
use override

從這裡就可以看出使用 override 關鍵字的好處了吧。

如何安全的傳遞 this 指標給其他函式

還記得在觀察者模式一文中,IObserver::Update 最後修改為傳 ISubject*,因此 CTSMCStockSubject 在 Notify 的實作是傳入 this 指標,程式碼如下:

class IObserver
{
public:
	virtual void Update(ISubject* pSubject) = 0;
};
class CTSMCStockSubject : public ISubject
{
public:
	virtual void Notify() override
	{
		for (auto& ob : m_observers)
		{
			ob->Update(this); // 傳入 this 指標
		}
	}
};

有沒有想過如果 CTAViewObserver 使用拉 (pull) 資料的方式,因此先將 ISubject* 儲存起來會有什麼問題呢?

class CTAViewObserver : public IObserver
{
public:
	virtual void Update(ISubject* pSubject) override
	{
		// 將 pSubject 儲存,稍後有時間再回主題類別拉 (pull) 資料
		m_mapStockID2Subject[pSubject->GetStockID()] = pSubject;
	}
};

答案就是,如果主題的生命週期短於觀察者,那麼到時觀察者存取 ISubject* 會導致未定義行為,因此我們會希望 Update 可以改為傳 std::shared_ptr< ISubject >,程式修改如下:

class IObserver
{
public:
	virtual void Update(const std::shared_ptr<ISubject>& pSubject) = 0;
};

那麼現在 CTSMCStockSubject::Notify 要怎麼呼叫 Update 呢?也就是怎麼將 this 指標轉為 std::shared_ptr<ISubject> 呢?

這時就是 std::enable_shared_from_this 派上用場的地方了,首先 CTSMCStockSubject 要繼承至 std::enable_shared_from_this<CTSMCStockSubject>,再來呼叫 Update 的地方修改為傳入 shared_from_this(),程式碼如下:

class CTSMCStockSubject : public ISubject, public std::enable_shared_from_this<CTSMCStockSubject>
{
public:
	virtual void Notify() override
	{
		for (auto& ob : m_observers)
		{
			ob->Update(shared_from_this());
		}
	}
};

最後要注意的地方是,現在 CTSMCStockSubject 必須要使用 std::shared_ptr 的方式才行。

int main()
{
	auto pTAViewObserver = std::make_shared<CTAViewObserver>();
	auto pTSMCStockSubject = std::make_shared<CTSMCStockSubject>();
	pTSMCStockSubject->Subscribe(pTAViewObserver);
	pTSMCStockSubject->Notify();
	pTSMCStockSubject->Unsubscribe(pTAViewObserver);
	return 0;
}

P.S. std::enable_shared_from_this 是個基底類別模板,模板參數是繼承的類別名稱,這個設計模式叫做 The Curiously Recurring Template Pattern (CRTP)

觀察者模式 (Observer pattern) 3

上一篇我們推導出需要 Observer 抽象基礎類別的原因,本篇我們來探討需要 Subject 抽象基礎類別的原因。

Observer pattern

主題通知觀察者資料更新有兩種方式,一種是直接將資料推 (push) 送給觀察者;另一種是先通知觀察者,等觀察者有時間再回主題拉 (pull) 資料。

如果是將資料推 (push) 送給觀察者,Update 需要修改為傳遞資料參數,如下:

class IObserver
{
public:
	virtual ~IObserver() = 0
	{}
public:
	virtual void Update(double dPrice) = 0;
};
// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(double dPrice) override
	{}
};

開發看盤軟體時,單一頁面觀察者不會只顯示一檔股票, 如果我們的觀察者對多個股票主題有興趣會怎麼樣呢?假設現在關注的主題有兩個,台積電與鴻海。

class CTSMCStockSubject final // 台積電
{
public:
	void Subscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.emplace(ob);
	}
	void Unsubscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.erase(ob);
	}
	void Notify()
	{
		for (auto& ob : m_observers)
		{
			double dPrice = 300;
			ob->Update(dPrice);
		}
	}
private:
	std::set<std::shared_ptr<IObserver>> m_observers;
};
class CFoxconnStockSubject final // 鴻海
{
public:
	void Notify()
	{
		for (auto& ob : m_observers)
		{
			double dPrice = 70;
			ob->Update(dPrice);
		}
	}
	// 其他程式碼省略...
};

注意 Notify 內呼叫的 Update 並沒有傳識別參數,那麼觀察者又怎麼知道是那個主題通知它呢?因此我們可能會修改為多傳一個股票代碼 (sStockID) 給 Update。

class IObserver
{
public:
	virtual ~IObserver() = 0
	{}
public:
	virtual void Update(const std::string& stockID, double dPrice) = 0;
};
// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(const std::string& stockID, double dPrice) override
	{}
};

那如果是先通知,等有時間再回主題拉 (pull) 資料呢?勢必 Update 就要傳入不同主題類別的指標,因此引進 Subject 抽象基礎類別就有必要。

class ISubject
{
public:
	virtual ~ISubject() = 0
	{}
public:
	virtual void Subscribe(const std::shared_ptr<IObserver>& ob) = 0;
	virtual void Unsubscribe(const std::shared_ptr<IObserver>& ob) = 0;
	virtual void Notify() = 0;
public:
	virtual std::string GetStockID() const = 0;
	virtual double GetPrice() const = 0;
};
// 台積電
class CTSMCStockSubject : public ISubject
{
	// 其他程式碼省略...
public:
	virtual void Notify() override
	{
		for (auto& ob : m_observers)
		{
			ob->Update(this);
		}
	}
public:
	virtual std::string GetStockID() const override
	{
		return "2330.TW";
	}
	virtual double GetPrice() const override
	{
		return 300;
	}
private:
	std::set<std::shared_ptr<IObserver>> m_observers;
};
// 鴻海
class CFoxconnStockSubject : public ISubject
{
	// 其他程式碼省略...
public:
	virtual std::string GetStockID() const override
	{
		return "2317.TW";
	}
	virtual double GetPrice() const override
	{
		return 70;
	}
};

Update 改為接收 ISubject*,其他參數都可以省略。

class IObserver
{
public:
	virtual ~IObserver() = 0
	{}
public:
	virtual void Update(ISubject* pSubject) = 0;
};
// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(ISubject* pSubject) override
	{
		// 這裡可以先將 pSubject 儲存,稍後有時間再回主題類別拉 (pull) 資料
		m_mapStockID2Subject[pSubject->GetStockID()] = pSubject;
	}
private:
	std::map<std::string, ISubject*> m_mapStockID2Subject;
};

到此為止說明了需要 Subject 抽象基礎類別的原因,下一篇會對設計做更深入的探討。

觀察者模式 (Observer pattern) 2

一個主題可能有多個觀察者,例如技術分析 ( 觀察者1 ),當日走勢 (觀察者 2) 等多個觀察者關注相同的主題。

Observer pattern

因為上一篇的設計沒有 Observer 抽象基礎類別,為了滿足多個觀察者的需求,或許會想到可以新增成員變數 ViewType 來區分不同的觀察者,擁有 $View_TA 的是技術分析,而擁有 $View_Tick 的是當日走勢。

enum class ViewType
{
	$View_TA, // 技術分析
	$View_Tick // 當日走勢
};
class CObserver final // 台積電資料的觀察者
{
public:
	CObserver(ViewType eType) : m_eType(eType)
	{}
public:
	void Update(CTSMCStockSubject* pSubject)
	{}
private:
	ViewType m_eType;
};
int main()
{
	auto ob1 = std::make_shared<CObserver>(ViewType::$View_TA); // 技術分析
	auto ob2 = std::make_shared<CObserver>(ViewType::$View_Tick); // 當日走勢
	CTSMCStockSubject subject;
	subject.Subscribe(ob1);
	subject.Subscribe(ob2);
	subject.Notify();
	subject.Unsubscribe(ob2);
	subject.Unsubscribe(ob1);
	return 0;
}

但這樣勢必 CObserver 內會有許多 if … else … 的判斷敘述去處理 $View_TA 及 $View_Tick 的分支 case,或許還有人進一步想到用狀態模式 (State pattern) 來重構 CObserver。

其實這些都是本末倒置,正確做法就是需要有 Observer 抽象基礎類別的存在,不同的觀察者繼承此抽象基礎類別並實作之,對應的處理邏輯寫在各自的衍生類別內,消除了 CObserver 內可能的 if (ViewType == $View_TA) else … 的判斷處理。

class IObserver // Observer 抽象基礎類別
{
public:
	virtual ~IObserver() = 0
	{}
public:
	virtual void Update(CTSMCStockSubject* pSubject) = 0;
};
// 技術分析
class CTAViewObserver : public IObserver
{
public:
	virtual void Update(CTSMCStockSubject* pSubject) override
	{}
};
// 商品盤勢
class CTickViewObserver : public IObserver
{
public:
	virtual void Update(CTSMCStockSubject* pSubject) override
	{}
};

CTSMCStockSubject 類別內所有的 CObserver 也要修改成 IObserver。

class CTSMCStockSubject final // 台積電資料的主題
{
public:
	void Subscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.emplace(ob);
	}
	void Unsubscribe(const std::shared_ptr<IObserver>& ob)
	{
		m_observers.erase(ob);
	}
	void Notify()
	{
		for (auto& ob : m_observers)
		{
			ob->Update(this);
		}
	}
private:
	std::set<std::shared_ptr<IObserver>> m_observers;
};

使用方式改為直接 new 出對應的 class,不必再使用 ViewType 區分,程式更為清楚明確。

int main()
{
	auto ob1 = std::make_shared<CTAViewObserver>(); // 技術分析
	auto ob2 = std::make_shared<CTickViewObserver>(); // 當日走勢
	CTSMCStockSubject subject;
	subject.Subscribe(ob1);
	subject.Subscribe(ob2);
	subject.Notify();
	subject.Unsubscribe(ob2);
	subject.Unsubscribe(ob1);
}

std::unique_lock 使用場景 1

std::unique_lock 是一種可以轉移所有權 (move constructor and move assignment) 的智慧指標 (smart pointer)。

假設在需要 thread-safe 的情況下,有一個大函式做了很多事,如類別成員函式 void BigFunc()。

class LockDemo
{
public:
	void BigFunc()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// prepare data
		// process data
		// clean data
	}
private:
	std::mutex m_mutex;
};

經過分析,發現此函式做了三件事 prepare data、process data 及 clean data。而根據單一職責原則 (Single Responsibility Principle),我們希望一個函式只做一件事,因此我們將此函式拆分為三個意義明確的小函式 (PrepareData, ProcessData and CleanData),並且 BigFunc只是轉呼叫此三個函式。

class LockDemo
{
public:
	void BigFunc()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		PrepareData();
		ProcessData();
		CleanData();
	}
private:
	void PrepareData()
	{
		// ...
	}
	void ProcessData()
	{
		// ...
	}
	void CleanData()
	{
		// ...
	}
private:
	std::mutex m_mutex;
};

又如果我們希望此三個函式可以由 caller 自由決定呼叫組合,我們會將此三個函式從 private function 改為 public function。

但如此一來,為了保證 thread-safe,必須將此三個函式都加上 std::unique_lock,而同一條 thread 鎖住自己兩次會造成 deadlock,因此 BigFunc 的使用就會有問題。

class LockDemo
{
public:
	void BigFunc()
	{
		std::unique_lock<std::mutex> lock(m_mutex); // lock 一次
		PrepareData(); // 呼叫此函式會再 lock 一次,造成 deadlock
		ProcessData();
		CleanData();
	}
public:
	void PrepareData()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// ...
	}
	void ProcessData()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// ...
	}
	void CleanData()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		// ...
	}
private:
	std::mutex m_mutex;
};

看來為了解決這個問題,我們只能將 BigFunc 的 std::unique_lock 移除。
P.S. 這裡不考慮使用 recursive_mutex,因為使用 recursive_mutex 通常表示設計上有問題,這會在別篇做解釋。

然而稍微不幸的是,如果在非常重視效率的情況下,此重構方式會讓呼叫 BigFunc 從建構及解構一次 std::unique_lock 變成了三次,而這可能是無法接受的效率損失,那麼還有其他方法嗎?有的,我們可以再次修改如下:

class LockDemo
{
public:
	std::unique_lock<std::mutex> GetLock()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		return lock;
	}
public:
	void BigFunc()
	{
		auto lock = GetLock();
		CleanData(ProcessData(PrepareData(std::move(lock))));
	}
public:
	std::unique_lock<std::mutex> PrepareData(std::unique_lock<std::mutex> lock)
	{
		// ...
		return lock;
	}
	std::unique_lock<std::mutex> ProcessData(std::unique_lock<std::mutex> lock)
	{
		// ...
		return lock;
	}
	std::unique_lock<std::mutex> CleanData(std::unique_lock<std::mutex> lock)
	{
		// ...
		return lock;
	}
private:
	std::mutex m_mutex;
};

利用 std::unique_lock 支援 move constructor,使用所有權轉移 (move constructor) 來取代建構子 (constructor) 呼叫,且解構子 (destructor) 的呼叫成本也有所降低 (因為是所有權轉移,中間的解構子呼叫不會執行 unlock)。

當然,此方法將使 caller 的呼叫變得複雜,另一個可以重構的方向是同時提供 non-thread-safe 及 thread-safe 版本的類別或函式,而這可以帶出裝飾者模式 (Decorate pattern) 的使用,將會在別篇介紹。