快速存取 tuple 的所有元素:std::apply

| | 0 Comments| 09:40
Categories:

之前在《讓函式回傳多個值: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++14index_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

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *