讓函式回傳多個值:std::tuple


在 C++ 的規範裡面,一個函式基本上只能回傳一個值;但是實際上,很多時候,我們會希望、也有需要讓一個函式可以回傳超過一個值。

這時候,常見的方法大概會是兩個方向:

  1. 把要回傳的值,以函式的參數的形式,來做傳遞
  2. 建立一個特殊的結構、或類別,來把要回傳的值打包起來

前者感覺應該比較像是 C 的寫法,像是微軟的 Kinect for Windows SDK v2 基本上就可以說是這種風格;他把回傳值都用來回傳執行的結果,而真正的資料,則都是以參數來做傳遞。

後者基本上比較像是 C++ 的物件導向,也算是很常見的、很好實作的;像是 std::minmax()參考)就是把兩個回傳的值、以 std::pair<> 的形式來做封包、回傳。

如果要看實際的例子的話,這邊就以讀取一張圖片來舉例;在 C++ 下,要描述一張 2D 的影像,一般至少要知道它的寬度、高度、有幾個通道,再來就是他的資料了(姑且假設每個通道的型別都固定是 8bit);所以,如果要撰寫一個 LoadImage() 的函式、讓他去讀一張圖檔,那他至少需要可以回傳上面四種值。

如果以「輸出參數」的形式來寫的話,應該會變成類似下面的樣子:

bool LoadImage(
	const std::string& sFilename,
	size_t& uWidth, size_t& uHeight, size_t& uchannel, char*& pData);
 
int main(int argc, char** argv)
{
	size_t	uWidth;
	size_t	uHeight;
	size_t	uChannel;
	char*	pData;
 
	if (LoadImage("test-file", uWidth, uHeight, uChannel, pData))
	{
		//
	}
}

其中,LoadImage() 這個函式只有第一個參數是輸入,而其他的參數都是輸出用的;但是如果在註解、文件沒有完整的說明的情況下,當參數更多的時候,會很難區分。

而如果希望清楚明確一點的話,常見的方法就是自己去定義一些詞,來強調參數到底是輸入還是輸出;例如:

#define _IN
#define _OUT
 
bool LoadImage(
	_IN const std::string& sFilename,
	_OUT size_t& uWidth, _OUT size_t& uHeight, _OUT size_t& uchannel,
	_OUT char*& pData);

而如果要比較好看的話,其實應該還是會是把這四個變數封包成一個物件來回傳。下面就是例子:

struct SImage
{
	size_t	uWidth;
	size_t	uHeight;
	size_t	uChannel;
	char*	pData;
};
 
SImage LoadImage(const std::string& sFilename);
 
int main(int argc, char** argv)
{
	SImage img = LoadImage("test-file");
}

這樣的方法容易閱讀、也不容易混淆,而且如果這樣的結構需要很頻繁地使用的話,基本上也可以讓整個程式更結構化。

但是,相對地,他缺點就是如果美個函式的回傳值得結構都不盡相同、而且沒有共通必要的話,那就可能會讓程式裡面多出一堆特殊、僅為了打包用的結構。


而如果不想定義額外的結構、又不希望把函式的參數變得太過複雜時,該怎麼辦呢?

如果再回傳值只有兩個的時候,其實 C++ 的標準函式庫裡的 std::pair<>參考)就已經夠用了;std::pair<> 提供了一個簡單的 template 類別,可以把兩個不同型別的數值包在一起。像是 std::minmax()參考)就是把最大值和最小值,以 std::pair<> 的形式回傳,他長的樣子是:

template< class T >
std::pair<const T&, const T&> minmax(const T& a, const T& b);

不過,std::pair<> 只能綁兩個數值,如果要封包在一起的東西超過兩個的話,其實就不太合用了。

也因此,在 C++11 的時候,標準函式庫就加入了 std::tuple<> 這個類別(參考),可以用來封包特定數量的資料。

#include <string>
#include <tuple>
 
std::tuple<size_t, size_t, size_t, char*>
	LoadImage(const std::string& sFilename)
{
	size_t	uWidth;
	size_t	uHeight;
	size_t	uChannel;
	char*	pData =nullptr;
 
	//
	return std::make_tuple(uWidth, uHeight, uChannel, pData);
}
 
int main(int argc, char** argv)
{
	auto img = LoadImage("test-file");
	size_t	uWidth		= std::get<0>(img);
	size_t	uHeight		= std::get<1>(img);
	size_t	uChannel	= std::get<2>(img);
	char*	pData		= std::get<3>(img);
}

在上面的例子可以看到,這邊 LoadImage() 的回傳值就是把四個變數封包起來的 std::tuple<size_t, size_t, size_t, char*>;要建立的話,最簡單的就是使用 std::make_tuple() 這個函式。

至於在拿到 std::tuple<> 這個類別之後,要把值一個一個取出來,一個方法就是使用 std::get<>() 這個函式。

而除了使用 std::get<>() 把裡面的資料一個一個拿出來外,其實也還可以使用 std::tie() 這個函式(參考,他也可以用來建立 std::tuple<> ),把所有的值都拿出來;下面就是它的使用例子:

size_t	uWidth;
size_t	uHeight;
size_t	uChannel;
char*	pData;
std::tie(uWidth, uHeight, uChannel, pData) = LoadImage("test-file");

這樣的寫法,在某方面來說,算是精簡不少。(註一)

而如果回傳的 std::tuple<> 裡面,有不想要的資料的話,也可以直接使用 std::ignore參考)來忽略掉;例如:

char*	pData;
std::tie(std::ignore, std::ignore, std::ignore, pData) = LoadImage("test-file");

這篇大概就寫這樣了。

基本上,個人是覺得,std::tuple<> 在某些狀況下,應該可以算是一個很實用的型別;它可以用來快速地把多個資料封包成一個,一方便傳遞。當然,相對地,如果封包的東西多的話,在沒有良好的文件的情況下,會難以辨視個別變數代表的意義。

例如,在上面的例子裡面,如果在沒有說明的情況下看到 std::tuple<size_t, size_t, size_t, char*> 這樣的資料,開發者也只能知道他有三個 size_t 和一個 char*,而很難知道他整體、個別代表的意義是什麼。

相較之下,SImage 這樣的結構由於有名稱,所以只要命名不要太糟糕,就算沒有文件,大多也都還能猜出來每個成員變數是什麼意思。

所以,是否要使用 std::tuple<> 呢?個人認為,還是得看場合、看狀況了。


註解:

  1. 而如果之後 C++17 的 structured bindings(參考)有實作可以用的話,則可以更方便地寫成:

    auto{uWidth, uHeight, uChannel, pData} = LoadImage("test-file");

    不過目前的 C++ 編譯器應該大多都還不支援這個寫法就是了。

  2. std::tuple_cat() 感覺也算是一個有趣的功能,可以把很多 std::tuple<> 合併成一個。(參考


參考:Returning multiple values from functions in C++

對「讓函式回傳多個值:std::tuple」的想法

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料