在 C++ 的規範裡面,一個函式基本上只能回傳一個值;但是實際上,很多時候,我們會希望、也有需要讓一個函式可以回傳超過一個值。
這時候,常見的方法大概會是兩個方向:
- 把要回傳的值,以函式的參數的形式,來做傳遞
- 建立一個特殊的結構、或類別,來把要回傳的值打包起來
前者感覺應該比較像是 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<> 呢?個人認為,還是得看場合、看狀況了。
註解:
-
而如果之後 C++17 的 structured bindings(參考)有實作可以用的話,則可以更方便地寫成:
auto{uWidth, uHeight, uChannel, pData} = LoadImage("test-file");
不過目前的 C++ 編譯器應該大多都還不支援這個寫法就是了。
-
std::tuple_cat() 感覺也算是一個有趣的功能,可以把很多 std::tuple<> 合併成一個。(參考)