ある金曜日の夜、趣味のプロジェクトの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つのテンプレート演算子で
int
、
char
、
float
、および
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
。これには、特に
int
と
char
が含まれています。 タイプ
char
で作成され、
int
を受け入れるラムダがビジター作成関数に最初に送信された場合、最初に呼び出され(そして正常に!)、
char
の場合には到達しません。 さらに、この問題は本当に致命的です:同じ
int
と
char
について、ラムダの順序を決定することは不可能であり(
int
と
char
両方について、型変換なしで適切な場所に転送されます)。
ただし、今では、ラムダとは何か、コンパイラによって展開されるものを覚えておく価値があります。 そして、再定義された
operator()
匿名構造に展開されます。 そして、構造がある場合、それから継承でき、その
operator()
は自動的に対応するスコープ内にあります。 そして、一度にすべての構造から継承する場合、すべての
operator()
が必要な場所に移動し、型が相互にキャストされている場合でも(上記の場合のように
int
と
char
)、コンパイラは各特定の型で呼び出すために必要な演算子を自動的に選択します。
それから-技術と可変テンプレートの問題:
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.