C++20 的 overload pattern

| | 0 Comments| 09:16
Categories:

之前 Heresy 曾經有寫過一篇《C++17 更通用的 union:variant》、來介紹 C++17 引進的 std::variant

而當時,在針對 std::visit() 做介紹的時候,有提到可以用 lambda 來組合出一個有針對不同型進行個別處理的 callable object 的方法;不過當時 C++ Reference 給的範例滿複雜的,Heresy 沒有認真去研究、所以後來是直接放棄了。 XD

而到了 C++20,由於語法本身的進化,所以要透過多個 lambda 來組成一個 callable object 的物件變得可以寫得很簡單了!這邊就再來看一下吧~

這邊要的定義實際上只有兩行:

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

這東西有什麼用呢?

這邊先回來看 std::variantstd::visit() 最基本的使用方法:

#include <iostream>
#include <variant>
 
struct PrintVisitor
{
  void operator()(int& i) const         { std::cout << "int: " << i; }
  void operator()(float& f) const       { std::cout << "float: " << f; }
  void operator()(std::string& s) const { std::cout << "string: " << s; }
};
 
int main(int argc, char** argv)
{
  std::variant<int, float, std::string> data{ "Hello" };
  std::visit(PrintVisitor(), data);
}

這邊 PrintVisitor 的物件基本上就是一個可以拿 intfloatstd::string 做引數的 callable object。

如果這東西只會用到一次、不想要另外定義一個型別出來的時候,可能就會想要用 lambda 來解決。在不區分型別的情況下,最簡單的寫法大概就會是使用 C++14 的 Generic lambda、或是 C++20 的 template lambda 吧?但是由於都是使用單一個 lambda 來處理,所以實際上和上面的方法來是有點不同。

而如果透過前面定義的 overload pattern 的話,這邊就可以很簡單地把多個 lambda 組成類似 PrintVisitor 的東西了。

這邊的寫法是:

#include <iostream>
#include <variant>
 
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
 
int main(int argc, char** argv)
{
  std::variant<int, float, std::string> data{ "Hello" };
  std::visit(
    overload{
      [](int& i)         { std::cout << "int: " << i; },
      [](float& f)       { std::cout << "float: " << f; },
      [](std::string& s) { std::cout << "string: " << s; }
    }, data);
}

其中,overload{ ... } 的效果就就等同於本來的 PrintVisitor() 了~

在個人來看,這樣寫的重點是不需要另外在別的地方定義要做什麼,可以把要做的實情直接寫在這邊;而好處呢,就是當下就可以馬上看得出來是要做什麼事、不需要另外再去看 PrintVisitor 的定義。

另外,他也可以拿其他的的可呼叫物件來用,例如:

overload{
  PrintVisitor(),
  [](size_t& s)  {
    std::cout << "size_t: " << s;
  }
}

這樣一來,他就可以支援 intfloatstd::stringsize_t 了~ 某種意義上,也變成一個相對簡單的擴充支援性的方法了。

而實際上,這樣的寫法不是只能用在 std::visit() 上,對於需要一個可以對應不同型別引數的可呼叫物件的情境下,都是可以用的!


回過頭來看,這邊兩行 overload 的定義到底是什麼意思呢?

第一行的定義加入換行後,基本上是:

template<class... Ts>
struct overload : Ts...
{
  using Ts::operator()...;
};

也就是定義一個名為 overload 的 template 結構,而他的 template 型別是以 template parameter pack 來讓他去繼承多個型別。

using Ts::operator()...; 則是用來讓基底型別的 operator() 可以直接被使用(參考)。

會需要加上這行的原因,是因為當 overload 同時繼承多個基底型別(這邊就是多個 lambda)的時候,由於都有各自的 operator(),雖然引數型別各自不同、但是還是會因為不知道該用哪個基底提供的 operator() 而導致無法編譯(不過 MSVC 雖然 IntelliSense 會警告,但是好像是可以接受就是)。

第二行則是:

template<class... Ts>
overload(Ts...) -> overload<Ts...>;

這個語法是 C++17 的 user-defined deduction guides(參考),它的形式是一個 template 函式、會針對傳入的引數產生對應的的型別。

而這邊的 -> 是 trailing return type 的形式(參考),不過這裡應該算是一個特別的語法就是了。

如果希望自己的 template 型別可以在建立物件時可以簡化的話,其實也可以透過這樣的語法來定義。


另外,如果是使用 C++20 的 template lambda 搭配 constexpr if 的話,則可以寫成:

std::visit([]<typename T>(T& val){
  if constexpr (std::is_same_v<T, int>)
  {
    std::cout << "int: " << val;
  }
  else if constexpr (std::is_same_v<T, float>)
  {
    std::cout << "float: " << val;
  }
  else if constexpr (std::is_same_v<T, std::string>)
  {
    std::cout << "string: " << val;
  }
}, data);

這樣的寫法基本上就是靠編一階段的 if – else 來做切換,如果在要執行的指令大多都相同、僅有少部分要針對不同型別做處理的時候,這樣的寫法應該會更簡單、方便。


參考:

Leave a Reply

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