引入

在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行左右的玩具就完成了.

推薦閱讀:

相关文章