之前在《讓函式回傳多個值:std::tuple》這篇文章中,曾經介紹過 C++11 引進的 std::tuple
這個可以用來儲存多種不同型別的類別了。
而由於 tuple
內部可以存放不同型別的資料,所以雖然可以用 std::tie()
或是 C++17 的 structured binding 來存取資料,但是都是需要針對特定的 tuple
來寫;實際上如果想針對所有類型的 tuple
寫個通用的函式,其實還滿麻煩的。
在 C++11 比較好的方法,應該就是 Parameter Pack、以類似遞迴的方式來寫了吧?而由於要透過 std::get<>()
這個函式去存取 tuple
的值的時候,索引值會需要是編譯階段的參數,所以會變得更為難寫…
像是如果要把 tuple
的所有元素展開,大概就得寫成:
#include <iostream> #include <tuple> #include <string> void print_tuple(int i, float f, double d, std::string s) { std::cout << "int: " << i << "\n" << "float: " << f << "\n" << "double: " << d << "\n" << "string: " << s << "\n"; } int main() { std::tuple<int,float,double,std::string> t{1,2.2f,3.3, "Hello" }; print_tuple(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t)); }
如果 tuple
很多的話,用 std::get<>()
來窮舉其實很麻煩。
而如果用 C++14 的 index_sequence
的話,則可以寫成:
#include <iostream> #include <tuple> #include <string> void print_tuple(int i, float f, double d, std::string_view s) { std::cout << "int: " << i << "\n" << "float: " << f << "\n" << "double: " << d << "\n" << "string: " << s << "\n"; } template<typename TTUPLE, std::size_t... I> void print_tuple(const TTUPLE& t, std::index_sequence<I...>) { print_tuple(std::get<I>(t)...); } int main() { std::tuple t{1,2.2f,3.3, "Hello" }; print_tuple(t, std::make_index_sequence<std::tuple_size<decltype(t)>::value>()); }
這邊雖然可以透過 index_sequence
來取代窮舉、但是老實說寫起來…沒有方便很多的感覺啊。 XD
所以為了簡化這部分程式的撰寫,C++17 也加入了 std::apply()
這個函式(文件),來簡化這部分的程式。
這邊如果改成用 std::apply()
來寫的話,就可以簡化成:
#include <iostream> #include <tuple> #include <string> void print_tuple(int i, float f, double d, std::string_view s) { std::cout << "int: " << i << "\n" << "float: " << f << "\n" << "double: " << d << "\n" << "string: " << s << "\n"; } int main() { std::tuple t{1,2.2f,3.3, "Hello" }; std::apply(print_tuple, t); }
他基本上就是把對於 index_sequence
的操作都包好了,所以在使用上變得簡單許多~
而如果再搭配 lambda 和 C++17 的 fold expression 的話,針對 tuple
的通用函式的撰寫也會變得更簡單。
比如說如果要幫 tuple
寫個通用版本的 operator<<
的話,大概會是:
#include <iostream> #include <tuple> int main() { std::tuple t{1, 2.5f, "Hello"}; std::apply( [](auto&& ... vals) { ((std::cout << vals << ","), ...); }, t ); }
這樣基本上也可以避免以往得用 parameter pack、然後用遞迴的方法來掃所有的引數的麻煩。
而如果想輸出的更好一點也是滿簡單的~下面就是定義一個針對 tuple
的通用版本的 operator<<
的例子:
#include <iostream> #include <tuple> template<typename ...Ts> std::ostream& operator<<(std::ostream& os, std::tuple<Ts ...> t) { std::apply( [&os](auto val, auto&& ... vals) { os << "[ " << val; ((os << ", " << vals), ...); os << " ]"; }, t); return os; } int main() { std::tuple t{1, 2.5f, "Hello"}; std::cout << t << "\n"; }
這邊丟給 apply()
的 lambda 會把第一個元素獨立當作 val
來使用,其他的則依舊還是 parameter pack 的形式;透過這樣的拆分,輸出就比較漂亮、也不用額外的判斷式了。
而如果想要拿到更細、針對每個元素做操作的話,其實也是可以透過再加一層 lambda 做到的:
#include <iostream> #include <tuple> #include <typeinfo> int main() { std::tuple t{1, 2.5f, "Hello"}; std::apply( [](auto&& ... vals) { (([](auto & v) { std::cout << typeid(v).name() << ":" << v << "\n"; }(vals)), ...); }, t); }
這邊就是把本來 fold 裡面的 (std::cout << vals << ",")
改成:
(([](auto & v) { std::cout << typeid(v).name() << ":" << v << "\n"; }(vals))
基本上就是建立一個 lambda、然後馬上去執行他;它的功用則是輸出變數的型別以及數值。
如果覺得 lambda 這樣寫太亂的話,他實際上和下面的程式是相同的:
#include <iostream> #include <tuple> #include <typeinfo> template<typename T> void output(T& v) { std::cout << typeid(T).name() << ":" << v << "\n"; } int main() { std::tuple t{1, 2.5f, "Hello"}; std::apply( [](auto&& ... vals) { (output(vals), ...); },t); }
另外,除了 std::apply()
外,C++17 其實也還多了一個 std::make_from_tuple()
,感覺上算是一個用來建構物件的特化版?(文件)
使用上大概會是下面的樣子:
#include <tuple> struct S { S(int i, float f) {} }; int main() { std::tuple t{1, 2.5f}; S s = std::make_from_tuple<S>(t); }
參考:C++ Templates: How to Iterate through std::tuple: std::apply and More