Boost用のシンプルなインラインビジター::バリアント

こんにちは、Habr。

ある金曜日の夜、趣味のプロジェクトの1つでエラー処理を書きました...それで、これは別の記事の紹介です。
一般的に、ある金曜日の夜、私はboost::variantを通過し、そこでデータを使って何かをする必要がありました。 boost::variantの非常に標準的なタスクであり、それを解決する標準的な(ただし非常にboost::static_visitor )方法は、 boost::static_visitorから継承された構造をoperator()オーバーロードし、 boost::apply_visitor渡すことboost::apply_visitor 。 そして、この美しい夜は、なんらかの理由で、この大量のコードを書くのが非常に面倒になり、訪問者を説明するためのよりシンプルで簡潔な方法を手に入れたかったのです。 これに由来するものは、カットの下で読むことができます。

したがって、標準的な方法は次のようになります。
 using Variant_t = boost::variant<int, char, std::string, QString, double, float>; template<typename ValType> struct EqualsToValTypeVisitor : boost::static_visitor<bool> { const ValType Value_; EqualsToValTypeVisitor (ValType val) : Value_ { val } { } bool operator() (const std::string& s) const { return Value_ == std::stoi (s); } bool operator() (const QString& s) const { return Value_ == s.toInt (); } template<typename T> bool operator() (T val) const { return Value_ == val; } }; void DoFoo (const Variant_t& var) { const int val = 42; if (boost::apply_visitor (EqualsToValTypeVisitor<int> { val }, var)) // ... } 

また、1つのテンプレート演算子でintcharfloat 、およびdouble 4つのケースを記述できるという事実を利用しました。そうしないと、さらに3つの演算子があり、コードがさらに肥大化し、さらにひどく見えます。

さらに、特定のタイプのハンドラー関数が短い場合、それらに別の構造を持たせ、それらが使用される関数から遠ざけるなど、ちょっと残念です。 また、コンストラクターを作成する必要があります。ビジターのアプリケーションのポイントからビジター自体にデータを転送する必要がある場合は、このデータのフィールドに入力する必要があります。コピー、リンクなどに従う必要があります。 これはすべて非常にいい臭いがし始めません。

自然な疑問が生じます:使用場所で直接訪問者を定義し、構文上のオーバーヘッドを最小限に抑えることは可能ですか? さて、右へ
 void DoFoo (const Variant_t& var) { const int val = 42; const bool isEqual = Visit (var, [&val] (const std::string& s) { return val == std::stoi (s); }, [&val] (const QString& s) { return val == s.toInt (); }, [&val] (auto other) { return other == val; }); } 


それはあなたができることが判明しました。

ソリューションはそれ自体が驚くほどシンプルでエレガントであり、すぐにそれを書くのは面白くありません(記事は非常に短くなります)ので、私がこの決定に至った経緯についても少し説明しますので、次の2つか3つの段落はスキップできます。

実装の詳細には触れませんが、 ここで突っ込むことができる最初の試みは、すべてのラムダをstd::tupleにプッシュし、それらを格納する独自のクラスのテンプレートoperator()順番に検索することoperator() operator()渡される引数を持つ関数。

このソリューションの明らかな欠点は、互いに縮小可能な型の致命的に不適切な処理と、ラムダがビジター作成関数に転送される順序への依存です。 したがって、上記のVariant_t考えてVariant_t 。これには、特にintcharが含まれています。 タイプcharで作成され、 intを受け入れるラムダがビジター作成関数に最初に送信された場合、最初に呼び出され(そして正常に!)、 charの場合には到達しません。 さらに、この問題は本当に致命的です:同じintcharについて、ラムダの順序を決定することは不可能であり( intchar両方について、型変換なしで適切な場所に転送されます)。

ただし、今では、ラムダとは何か、コンパイラによって展開されるものを覚えておく価値があります。 そして、再定義されたoperator()匿名構造に展開されます。 そして、構造がある場合、それから継承でき、そのoperator()は自動的に対応するスコープ内にあります。 そして、一度にすべての構造から継承する場合、すべてのoperator()が必要な場所に移動し、型が相互にキャストされている場合でも(上記の場合のようにintchar )、コンパイラは各特定の型で呼び出すために必要な演算子を自動的に選択します。

それから-技術と可変テンプレートの問題:
 namespace detail { template<typename... Args> struct Visitor : Args... // ,     variadic pack { Visitor (Args&&... args) : Args { std::forward<Args> (args) }... //    { } }; } 


boost::variantとラムダのセットを取り、このvariantを訪問する関数を書きましょう:
 template<typename Variant, typename... Args> auto Visit (const Variant& v, Args&&... args) { return boost::apply_visitor (detail::Visitor<Args...> { std::forward<Args> (args)... }, v); } 


ああ、コンパイルエラーが発生しました。 apply_visitorは、少なくとも私のバージョンのBoost 1.57では、相続人boost::static_visitorを取得することを期待しています(つまり、C ++ 14モードでの戻り値型の自動戻りのサポートが後で追加されました)。

戻り型を取得する方法は? たとえば、リストから最初のラムダを取得し、デフォルトで構築されたオブジェクトで呼び出すことができます。
 template<typename Variant, typename Head, typename... TailArgs> auto Visit (const Variant& v, Head&& head, TailArgs&&... args) { using R_t = decltype (head ({})); //return boost::apply_visitor (detail::Visitor<Head, TailArgs...> { std::forward<Head> (head), std::forward<TailArgs> (args)... }, v); } 

同時に、当然、すべてのラムダが同じ型を返すと仮定します(より正確には、返されるすべての型は相互に変換可能です)。

このソリューションの問題は、このオブジェクト自体にデフォルトのコンストラクターがない場合があることです。 std::declvalは、最初のラムダによって事前に受け入れられるタイプが事前にわからないため、ここでも役に立ちません。 variantタイプのリストから行のすべてのタイプでそれを呼び出そうとすると、非常に無愛想で冗長になります。

代わりに、逆を行います。 variant型のリストから最初の型を取得し、それを使用して既に構築されたVisitorを呼び出します。 これは、訪問者がvariant型のいずれかを処理できる必要があるため、機能することが保証されています。 だから:
 template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) -> { using R_t = decltype (detail::Visitor<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())); //return boost::apply_visitor (detail::Visitor<Args...> { std::forward<Args> (args)... }, v); } 


ただし、 Visitor自体はboost::static_visitor<R_t>から継承する必要があり、 R_tこの時点でR_tです。 それは、 Visitorを2つのクラスに分割することで解決するのは非常に簡単です。1つはラムダからの継承とoperator()の集約を処理し、もう1つはboost::static_visitor実装します。

合計
 namespace detail { template<typename... Args> struct VisitorBase : Args... { VisitorBase (Args&&... args) : Args { std::forward<Args> (args) }... { } }; template<typename R, typename... Args> struct Visitor : boost::static_visitor<R>, VisitorBase<Args...> { using VisitorBase<Args...>::VisitorBase; }; } template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) { using R_t = decltype (detail::VisitorBase<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())); return boost::apply_visitor (detail::Visitor<R_t, Args...> { std::forward<Args> (args)... }, v); } 

C ++ 11との互換性のために、フォームの末尾の戻り値の型を追加できます
 template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) -> decltype (detail::VisitorBase<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())) 


素晴らしいボーナスは、コピー不可のラムダを使用できることです(たとえば、C ++ 14スタイルのunique_ptrをキャプチャします)。
 #define NC nc = std::unique_ptr<int> {} Variant_t v { 'a' }; const auto& asQString = Visit (v, [NC] (const std::string& s) { return QString::fromStdString (s); }, [NC] (const QString& s) { return s; }, [NC] (auto val) { return QString::fromNumber (val); }); 


欠点は、スタイルのより細かいパターンマッチングが不可能であることです。
 template<typename T> void operator() (const std::vector<T>& vec) { //... } 

残念ながら、 [] (const std::vector& vec) {} . C++17. [] (const std::vector& vec) {} . C++17.

Source: https://habr.com/ru/post/J270689/


All Articles