把玩C++模板 - 簡化Variant
引入
在C++中並沒有原生的ADT支持,且C中enum+union的組合也並不適用於C++,於是標準庫引入了用模板實現的tagged union - std::variant.但是這玩意用起來卻不是很直觀,接下來就介紹一些小技巧來改善這一點.
延續一貫的風格,先列一個簡單的問題:訪問一個簡單的variant:
std::variant<Shitizen, Citizen> v;
現狀
首先來看看目前標準庫提供的方案,第一種:
if(std::hold_alternative<Shitizen>(v))
std::get<Shitizen>(v); ...
else
std::get<Citizen>(v); ...
第二種:
struct visitor
{
void operator()(Shitizen x, Shitizen y) {...}
void operator()(Citizen x, Citizen y) {...}
}
std::visit(visitor{}, v, v);
可以看到第一種不能接受隱式轉換.也不能接受多個參數進行分派,而第二種代碼需要單獨定義一個visitor類.且這兩種方式都有"樣板代碼"的存在,不是很清真.
改進
好在第二種方法還有改進的餘地:
template<class... Fs> struct overload : Fs... { using Fs::operator()...; };
template<class... Fs> overload(Fs...) -> overload<Fs...>;
std::visit(overload
{
[](Shitizen x){ ... },
[](Citizen x){ ... },
[](auto...){ ... }
}, v);
這裡利用C++的黑科技:User-defined deduction guides + Using Unpack 實現了overload類,讓我們可以原地讓lambda表達式重載到一起,避免了額外寫一個visitor,進一步簡化了代碼.(這段在cppreference上可以看到).
然而,這個用法看起來還是有點彆扭,不如更進一步,調整一下語法:
- 不要是一個函數調用,要像原生的語法
- 被匹配的目標放前面,而不是後面
解決辦法如下:
- 使用運算符重載來去掉調用的括弧
- 用initializer_list來形成大括弧
- 使用宏來遮蔽樣板代碼
- 用tuple來暫存參數,用lambda調整參數的順序
類似的方法也可以用來造其他DSL,代碼如下:
template<class... Ts>
struct matcher
{
std::tuple<Ts...> vs;
template<class... Vs> constexpr matcher(Vs&&... vs) : vs(std::forward<Vs>(vs)...) {}
template<class Fs> constexpr auto operator->*(Fs&& f) const
{
auto curry = [&](auto&&... vs) { return std::visit(std::forward<Fs>(f), vs...); };
return std::apply(curry, std::move(vs));
}
};
template<class... Ts> matcher(Ts&&...)->matcher<Ts&&...>;
#define Match(...) matcher{__VA_ARGS__} ->* overload
使用的代碼如下:
Match(v)
{
[](Shitizen x) { ... }
, [](Citizen x) { ... }
};
結束
至此,第三個10行左右的玩具就完成了.
推薦閱讀: