相較於其他程式語言,C++ 的函數參數傳遞方式有很多種方法,如傳值(value)、傳址(address/pointer),及傳參考(reference)等,再加上 const 修飾詞後又有不同的變化。

  C++11 引入的 unique_ptr 及 shared_ptr 等智慧指標,也同樣有傳值或傳參考等方式。雖然很多手段都可以達到相同目的,但如果透過傳遞方式的不同,就可以表現出函數設計者想傳達的「語意」,那一定會是比較好的設計,同時好的設計也能借助編譯器的輔助來降低程式出錯的機會。

  針對各種不同需求及使用情境,這篇整理我蒐集到的建議實作方式,主要源自於 C++ Core Guideline,提供給各位參考。

 

原則: 函數只在想控制物件生命時,才將 Smart Pointer 作為參數

  這個原則是剛接觸智慧指標時容易沒注意到的地方,在我寫了一段時間的 C# 後,回來寫使用 C++11 的專案時也曾混淆過。

  雖然智慧指標所表現的行為和許多具有垃圾回收(GC)機制的語言中的物件參考非常類似,但在語意上卻有所不同,智慧指標所表達的是該物件生命週期控制者的概念,但其他程式語言在這部分的概念就沒有那麼強烈。

  根據這項原則,如果函數想操作某個智慧指標所指的物件,應直接傳遞該物件的參考,而非智慧指標:

好的設計
void Callee(Object &obj)
{
	// 或者是: void Callee(const Object &obj)
	obj.DoSomething();
}

void Caller()
{
	unique_ptr<Object> uniqueObj = make_unique<Object>();
	Callee(*uniqueObj);
	// Object obj;
	// Callee(obj); // OK !
}
不建議的設計
void Callee(unique_ptr<Object>& obj)
{
	obj->DoSomething();
}

void Caller()
{
	unique_ptr<Object> uniqueObj = make_unique<Object>();
	Callee(uniqueObj);
	// Object obj;
	// Callee(obj); // Error !!
}

  直接傳遞物件的 reference 或是 const reference 主要有幾個理由:

  1. 相容性比較好:

    如果函數的參數是 unique_ptr 的參考,但 Caller 卻不是透過 unique_ptr 管理時,就會無法使用該函數了。把物件直接在宣告在 stack 上,或是 Caller 使用 share_ptr 來管理物件都會有問題。

  2. 意圖比較清晰 :

    由於接收的是物件的 reference,因此 Caller 可以相信該函數不會去控制該物件的生命 (雖然該函數也是可以直接把物件給強制 delete 掉,但這通常是頗糟的設計…)

  根據這個原則,我們可以知道使用 const unique_ptr& 作為參數的函數可能是壞味道,因為被宣告成 const 的智慧指標並不能控制或轉移物件的生命週期。

  const shared_ptr& 應該也不太常見,比較合理的解釋是該函數想使用 use_count() 取得 shared_ptr 的 reference count,但這種需求我覺得應該是蠻少的。

// 通常是 bad design ...
void Callee(const unique_ptr<Object>& obj);
void Callee(const shared_ptr<Object>& obj);

 

Smart Pointer 常見的參數傳遞方式

  知道主要原則以後,何時需要及如何傳遞 Smart Pointer 就變得比較容易瞭解了,大致上會有以下幾種:

void Callee(unique_ptr<Object> obj);		// (1)
void Callee(shared_ptr<Object> obj);		// (2)
void Callee(unique_ptr<Object>& obj);		// (3)
void Callee(shared_ptr<Object>& obj);		// (4)
void Callee(const shared_ptr<Object>& obj);	// (5) 少見

// (6) 包含 const Object& obj, Object*, const Object* 等
void Callee(Object& obj);

 

(1) (2) 以傳值 (value) 方式傳遞 Smart Pointer

  當 Smart Pointer 以傳值方式出現在函數參數時,通常代表該函數想取得該物件生命週期控制權。這很容易理解,因為 unique_ptr 不能被複製,因此 caller 必須使用 move 語意來呼叫此函數,同時也代表著物件控制權的轉移;而 shared_ptr 的 reference count 會增加,因此物件的控制者增加也是能理解的事。

(3) (4) 透過參考 (reference) 來傳遞 Smart Pointer

  當 Smart Pointer 以傳參考方式出現在函數參數時,代表著該函數可能想重置該 Smart Pointer 的所指對象。重置可能是刪除物件,或是讓該 Smart Pointer 指向其他的物件,傳 reference 就是達成此目的最好的選擇,當然傳遞 Smart Pointer 的指標 (shared_ptr*) 也不是不可以,但傳參考通常會好用一點。

(5) 傳遞 const shared_ptr&

  我個人是覺得這個使用情境應該是很少,除了想拿 shared_ptr 的 refcount 以外 … 如果有什麼看法也歡迎提供給我。

(6) 直接傳遞所指物件型別的參考或指標

  這個會特別提及主要是想提醒不要忘了還有這個選擇,當沒有要控制物件生命週期時就不需要傳遞 Smart Pointer,可以直接傳遞該物件的參考或指標就好。

 

其他

  當然 C++ 還有其他的傳遞方式,例如傳遞右值參考(rvalue reference)等,但這通常也不是什麼好主意,例如以下的程式的 Caller 部分

void Callee(unique_ptr<Object>&& obj)
{
	obj->DoSomething();
}

void Caller()
{
	unique_ptr<Object> uniqueObj = make_unique<Object>();
	// 因為函數接收的是 rvalue reference,因此你只能使用 move 來呼叫該函數
	// 乍看之下已經把 uniqueObj 的控制權轉移給 Callee 了,但實際上沒有!!
	Callee(move(uniqueObj));
}

  乍看之下 uniqueObj 的控制權似乎已經轉移給 Callee 了,但實際上並沒有,非常容易誤導人。

  關於這份文件的說明,如果有其他看法或覺得不恰當的地方,也歡迎留言給我。以下是本文的幾個主要參考資料來源: