如何使用 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)

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) 的使用,將會在別篇介紹。

C++ 17 的 Read/Write Lock 使用方法

開發看盤軟體時,資料通常是讀取的頻率大於寫入的頻率,例如收到一筆股價 Tick,寫入資料結構一次,然後在多種圖形顯示,也就是讀取多次。

在 C++ 17 之前,要使用 Read/Write Lock 只能自己實現或使用系統提供的 API,但現在情況有所改變,以下就是 C++ 17 讀寫鎖的使用方法:

#include <shared_mutex>

std::shared_mutex g_mutex;

Write Lock 這樣寫

std::unique_lock<std::shared_mutex> wLock(g_mutex);

Read Lock 這樣寫

std::shared_lock<std::shared_mutex> rLock(g_mutex);

可以發現 C++ 17 Read/Write lock 的使用就是這麼平易近人且直觀。

但有一個地方要注意,一般我們都是在成員函式裡使用 std::shared_mutex,所以會將它宣告為成員變數,另外我們會用 Set function 來改變成員變數,Get function 讀取成員變數,所以會宣告 Get function 為 const 成員函式,例子如下:

#include <shared_mutex>

class CStockInfo
{
public:
	void SetPrice(double dPrice)
	{
		std::unique_lock<std::shared_mutex> wLock(m_mutex);
		m_dPrice = dPrice;
	}

	double GetPrice() const
	{
		std::shared_lock<std::shared_mutex> rLock(m_mutex);
		return m_dPrice;
	}

private:
	double m_dPrice;
	std::shared_mutex m_mutex;
};

但此時編譯程式會發現編譯器在 GetPrice 報錯,因為成員函式宣告為 const,此時不明就裡的程式設計師會乾脆將 const 拿掉,這是錯誤的,正確的做法是在宣告 std::shared_mutex 變數前加上 mutable,如此就告訴編譯器它是邏輯上的常量了。

#include <shared_mutex>

class CStockInfo
{
...
private:
	double m_dPrice;
	mutable std::shared_mutex m_mutex; // 宣告為 mutable

Task Queue 任務隊列 – 生產者與消費者

在開發看盤軟體時,時常會需要用到一種 Task Queue,可以將收到的任務存放起來,供稍後取出來處理,例如收到股價資料要依序處理。

又或者在觀察者模式中訂閱了某檔個股,而個股會有許多事件需要通知觀察者,例如歷史股價準備完成、即時股價到達或資料有誤需重抓等,觀察者可能來不及處理,需要先將事件存入 Task Queue 以便稍後處理。

另外不難想像 Task Queue 的兩端就像是生產者與消費者的關係,且可能同時存在多個生產者及多個消費者,因此 Task Queue 還必須是 Thread-safe。

Task Queue
任務隊列

以下為精簡後的 code,並且假設 Task Queue 存放的型別為 std::string,讓我們來看程式的關鍵部分,也就是 Put function 和 Take function 的寫法:

#include 
#include 
#include 
#include 

class CTaskQueue
{
public:
	void Put(const std::string& s)
	{
		{
			std::lock_guard lock(m_mutex);
			m_queue.push(s);
		}

		m_condNotEmpty.notify_one();
	}

	std::string Take()
	{
		std::unique_lock lock(m_mutex);
		m_condNotEmpty.wait(lock, [this]
			{
				return !this->m_queue.empty();
			});

		std::string s = m_queue.front();
		m_queue.pop();
		return s;
	}

private:
	std::mutex m_mutex;
	std::queue m_queue;
	std::condition_variable m_condNotEmpty;
};

首先來看 Take function,在 m_queue 為空的時候,消費者 thread 不能取得資料,因此有必要使用 std::condition_variable 根據 m_queue 是否為空的條件來暫停消費者 thread 執行,直到 m_queue 不為空時才被喚醒。注意這裡使用 std::condition_variable 的 wait 成員函式,當表達式為 true 時會繼續執行,因此要使用 !this->m_queue.empty() 來當作判斷式。

還有要注意的一點是,當消費者 thread 被暫停後,m_mutex 會被釋放,不然程式就會永遠卡住了,請各位暫停並思考一下此處奧妙,或繼續往下看也會有解答。

那麼由誰來喚醒消費者 thread 呢?接下來就將目光移到 Put function。因為需要 Thread-safe,所以 m_queue.push(s) 需要使用 std::mutex 來保護,這沒有問題,並且請注意,這裡的生產者 thread 是可以搶到 m_mutex 的,這也是為什麼 std::condition_variable 條件不滿足需要釋放 m_mutex,不釋放則程式會永遠卡住。

當生產者 thread 將資料放進 Task Queue 後,m_condNotEmpty.notify_one() 會喚醒一個暫停中的消費者 thread,注意此處 m_condNotEmpty.notify_one() 的呼叫是在 { … } 之外,不在外面的話,因為 m_mutex 尚未被 Put function 釋放,有可能 Take function 內被喚醒的消費者 thread 會搶不到 m_mutex,又會短暫進入暫停,程式執行效率會稍慢一些。

好了,這就是全部了,謝謝各位。以下附上完整的程式碼,完整的程式碼複雜許多,未來再細細解釋吧!

#pragma once

#include 
#include 
#include 

template
class CTaskQueue final
{
public:
	CTaskQueue() : m_bStop(true), m_mutex(), m_queue(), m_condNotEmpty() {}
	CTaskQueue(const CTaskQueue&) = delete;
	CTaskQueue(CTaskQueue&&) = delete;
	CTaskQueue& operator=(const CTaskQueue&) = delete;
	CTaskQueue& operator=(CTaskQueue&&) = delete;
	~CTaskQueue()
	{
		Stop();
	}

public:
	void Start()
	{
		std::lock_guard lock(m_mutex);
		m_bStop = false;
	}

	void Stop()
	{
		{
			std::lock_guard lock(m_mutex);
			m_bStop = true;
		}

		m_condNotEmpty.notify_all();
	}

	void Clear()
	{
		std::lock_guard lock(m_mutex);
		m_queue.clear();
	}

	void Put(const T& t)
	{
		Add(t);
	}

	void Put(T&& t)
	{
		Add(std::move(t));
	}

	T Take()
	{
		std::unique_lock lock(m_mutex);
		m_condNotEmpty.wait(lock, [this]
			{
				return !this->m_queue.empty() || this->m_bStop;
			});

		if (m_bStop)
		{
			return T();
		}

		auto t = std::move(m_queue.front());
		m_queue.pop();
		return t;
	}

	auto TakeAll()
	{
		std::unique_lock lock(m_mutex);
		m_condNotEmpty.wait(lock, [this]
			{
				return !this->m_queue.empty() || this->m_bStop;
			});

		if (m_bStop)
		{
			return std::queue();
		}

		return std::move(m_queue);
	}

private:
	template
	void Add(T&& t)
	{
		{
			std::lock_guard lock(m_mutex);
			if (m_bStop)
			{
				return;
			}

			m_queue.push(std::forward(t));
		}

		m_condNotEmpty.notify_one();
	}

private:
	bool m_bStop;
	std::mutex m_mutex;
	std::queue m_queue;
	std::condition_variable m_condNotEmpty;
};