之前 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::variant
和 std::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
的物件基本上就是一個可以拿 int
、float
、std::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; } }
這樣一來,他就可以支援 int
、float
、std::string
和 size_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 來做切換,如果在要執行的指令大多都相同、僅有少部分要針對不同型別做處理的時候,這樣的寫法應該會更簡單、方便。
參考: