web-dev-qa-db-ja.com

参照による複数の関数オブジェクトのオーバーロード

C++ 17では、overload(fs...)関数を実装するのは簡単です。この関数は、_fs..._を満たす任意の数の引数が与えられると、 FunctionObject を返します。 関数オブジェクト _fs..._のオーバーロードのように動作します。例:

_template <typename... Ts>
struct overloader : Ts...
{
    template <typename... TArgs>
    overloader(TArgs&&... xs) : Ts{forward<TArgs>(xs)}...
    {
    }

    using Ts::operator()...;
};

template <typename... Ts>
auto overload(Ts&&... xs)
{
    return overloader<decay_t<Ts>...>{forward<Ts>(xs)...};
}

int main()
{
    auto o = overload([](char){ cout << "CHAR"; }, 
                      [](int) { cout << "INT";  });

    o('a'); // prints "CHAR"
    o(0);   // prints "INT"
}
_

ワンドボックスの実例


上記のoverloaderは_Ts..._を継承しているため、機能させるには関数オブジェクトをコピーまたは移動する必要があります。 同じオーバーロード動作を提供するものが必要ですが、渡された関数オブジェクトへの参照のみです。

その架空の関数をref_overload(fs...)と呼びましょう。私の試みは、次のように_std::reference_wrapper_と_std::ref_を使用していました。

_template <typename... Ts>
auto ref_overload(Ts&... xs)
{
    return overloader<reference_wrapper<Ts>...>{ref(xs)...};
}
_

簡単そうですね。

_int main()
{
    auto l0 = [](char){ cout << "CHAR"; };
    auto l1 = [](int) { cout << "INT";  };

    auto o = ref_overload(l0, l1);

    o('a'); // BOOM
    o(0);
}
_
_error: call of '(overloader<...>) (char)' is ambiguous
 o('a'); // BOOM
      ^
_

ワンドボックスの実例

それが機能しない理由は単純です: std::reference_wrapper::operator()可変個引数関数テンプレート、これは再生されませんオーバーロードでうまくいきます。

using Ts::operator()...構文を使用するには、FunctionObjectを満たすために_Ts..._が必要です。独自のFunctionObjectラッパーを作成しようとすると、同じ問題が発生します。

_template <typename TF>
struct function_ref
{
    TF& _f;
    decltype(auto) operator()(/* ??? */);
};
_

"コンパイラを表現する方法がないので、_???_にTF::operator()"とまったく同じ引数を入力してください。可変個引数関数を使用する必要があります。テンプレート、何も解決しません。

また、overload(...)に渡される関数の1つがfunction templateまたはである可能性があるため、 _boost::function_traits_ のようなものは使用できません。オーバーロードされた関数オブジェクトそれ自体!

したがって、私の質問は次のとおりです。任意の数の_fs..._関数オブジェクトが与えられると、動作する新しい関数オブジェクトを返すref_overload(fs...)関数を実装する方法はありますか_fs..._のオーバーロードのようですが、コピー/移動する代わりに_fs..._を参照しますか?

39
Vittorio Romeo

さて、これが計画です。質問に示されているように、継承に基づいて宣言を使用した場合に選択されるoperator()オーバーロードを含む関数オブジェクトを決定します。 。オーバーロードの解決が成功した後に発生する、暗黙的なオブジェクトパラメータの派生からベースへの変換にあいまいさを強制することにより、(評価されていないコンテキストで)これを実行します。この動作は標準で指定されています。N4659 [namespace.udecl]/16 および 18 を参照してください。

基本的に、各関数オブジェクトを追加の基本クラスサブオブジェクトとして順番に追加します。オーバーロードの解決が成功した呼び出しの場合、勝ったオーバーロードを含まない関数オブジェクトの基本的なあいまいさを作成しても、何も変更されません(呼び出しは引き続き成功します)。ただし、複製されたベースに選択されたオーバーロードが含まれている場合、呼び出しは失敗します。これにより、SFINAEコンテキストを使用できます。次に、対応する参照を介して通話を転送します。

_#include <cstddef>
#include <type_traits>
#include <Tuple>
#include <iostream>

template<class... Ts> 
struct ref_overloader
{
   static_assert(sizeof...(Ts) > 1, "what are you overloading?");

   ref_overloader(Ts&... ts) : refs{ts...} { }
   std::Tuple<Ts&...> refs;

   template<class... Us> 
   decltype(auto) operator()(Us&&... us)
   {
      constexpr bool checks[] = {over_fails<Ts, pack<Us...>>::value...};
      static_assert(over_succeeds(checks), "overload resolution failure");
      return std::get<choose_obj(checks)>(refs)(std::forward<Us>(us)...);
   }

private:
   template<class...> 
   struct pack { };

   template<int Tag, class U> 
   struct over_base : U { };

   template<int Tag, class... Us> 
   struct over_base<Tag, ref_overloader<Us...>> : Us... 
   { 
       using Us::operator()...; // allow composition
   }; 

   template<class U> 
   using add_base = over_base<1, 
       ref_overloader<
           over_base<2, U>, 
           over_base<1, Ts>...
       >
   >&; // final & makes declval an lvalue

   template<class U, class P, class V = void> 
   struct over_fails : std::true_type { };

   template<class U, class... Us> 
   struct over_fails<U, pack<Us...>,
      std::void_t<decltype(
          std::declval<add_base<U>>()(std::declval<Us>()...)
      )>> : std::false_type 
   { 
   };

   // For a call for which overload resolution would normally succeed, 
   // only one check must indicate failure.
   static constexpr bool over_succeeds(const bool (& checks)[sizeof...(Ts)]) 
   { 
       return !(checks[0] && checks[1]); 
   }

   static constexpr std::size_t choose_obj(const bool (& checks)[sizeof...(Ts)])
   {
      for(std::size_t i = 0; i < sizeof...(Ts); ++i)
         if(checks[i]) return i;
      throw "something's wrong with overload resolution here";
   }
};

template<class... Ts> auto ref_overload(Ts&... ts)
{
   return ref_overloader<Ts...>{ts...};
}


// quick test; Barry's example is a very good one

struct A { template <class T> void operator()(T) { std::cout << "A\n"; } };
struct B { template <class T> void operator()(T*) { std::cout << "B\n"; } };

int main()
{
   A a;
   B b;
   auto c = [](int*) { std::cout << "C\n"; };
   auto d = [](int*) mutable { std::cout << "D\n"; };
   auto e = [](char*) mutable { std::cout << "E\n"; };
   int* p = nullptr;
   auto ro1 = ref_overload(a, b);
   ro1(p); // B
   ref_overload(a, b, c)(p); // B, because the lambda's operator() is const
   ref_overload(a, b, d)(p); // D
   // composition
   ref_overload(ro1, d)(p); // D
   ref_overload(ro1, e)(p); // B
}
_

ワンドボックスの実例


警告:

  • 継承に基づくオーバーロードは必要ありませんが、必要に応じてこれらの関数オブジェクトから継承できると想定しています。そのような派生オブジェクトは作成されませんが、評価されていないコンテキストで実行されるチェックは、これが可能であることに依存しています。これらのオーバーロードを同じスコープに入れて、オーバーロード解決を適用できるようにする他の方法は考えられません。
  • 呼び出しへの引数に対して転送が正しく機能することを前提としています。ターゲットオブジェクトへの参照を保持していることを考えると、何らかの転送なしでこれがどのように機能するかわかりません。したがって、これは必須の要件のようです。
  • これは現在Clangで機能します。 GCCの場合、依存している派生からベースへの変換はSFINAEコンテキストではないように見えるため、ハードエラーがトリガーされます。私の知る限り、これは正しくありません。 MSVCは非常に役立ち、私たちの呼び出しを明確にします。最初に来る基本クラスのサブオブジェクトを選択するだけのようです。そこに、それは機能します-何が好きではないのですか? (MSVCは、他のC++ 17機能もサポートしていないため、現時点では問題との関連性は低くなっています)。
  • 構成は、いくつかの特別な予防措置を通じて機能します-仮想の継承ベースのオーバーロードをテストする場合、_ref_overloader_はその構成関数オブジェクトにアンラップされるため、それらのoperator()は転送operator()。 _ref_overloader_ sを作成しようとする他のオーバーロードは、同様のことを行わない限り、明らかに失敗します。

いくつかの便利なビット:

  • 素敵な簡略化された例 by Vittorio あいまいな基本アイデアの実行を示しています。
  • _add_base_の実装について:_over_base_の_ref_overloader_の部分特殊化は、他の_ref_overloader_を含む_ref_overloader_を有効にするために上記の「アンラップ」を行います。それが整ったら、それを再利用して_add_base_をビルドしましたが、これはちょっとしたハックです。 _add_base_は実際には_inheritance_overloader<over_base<2, U>, over_base<1, Ts>...>_のようなものを意味しますが、同じことを行う別のテンプレートを定義したくありませんでした。
  • _over_succeeds_でのその奇妙なテストについて:論理は、通常の場合(あいまいなベースが追加されていない)で過負荷の解決が失敗した場合、追加されたベースに関係なく、すべての「インストルメントされた」ケースでも失敗するというものですしたがって、checks配列にはtrue要素のみが含まれます。逆に、オーバーロード解決が通常の場合に成功する場合、1つを除く他のすべての場合にも成功するため、checksには1つのtrue要素が含まれ、他のすべての要素はfalseに等しくなります。

    checksの値がこのように均一であることを考えると、最初の2つの要素だけを見ることができます。両方がtrueの場合、これは通常の場合の過負荷解決の失敗を示します。他のすべての組み合わせは、解決の成功を示します。これは怠惰な解決策です。実稼働実装では、おそらくchecksに期待される構成が実際に含まれていることを確認するための包括的なテストに行きます。


GCCのバグレポート 、提出者 Vittorio

MSVCのバグレポート

28
bogdan

一般的には、C++ 17でもそういうことはできないと思います。最も不快なケースを考えてみましょう。

_struct A {
    template <class T> int operator()(T );
} a;

struct B {
    template <class T> int operator()(T* );
} b;

ref_overload(a, b)(new int);
_

どうすればそれを機能させることができますか?両方のタイプが_int*_で呼び出し可能であることを確認できましたが、両方のoperator()はテンプレートであるため、それらの署名を選択できません。可能であったとしても、推定されるパラメーター自体は同一です。どちらの関数も_int*_を取ります。 bを呼び出すことをどのように知っていますか?

このケースを正しくするために、基本的に行う必要があるのは、呼び出し演算子に戻り値の型を挿入することです。タイプを作成できる場合:

_struct A' {
    template <class T> index_<0> operator()(T );
};

struct B' {
    template <class T> index_<1> operator()(T* );
};
_

次に、decltype(overload(declval<A'>(), declval<B'>()))::valueを使用して、自分自身を呼び出す参照を選択できます。

simplestの場合-ABの両方(およびCと.。 )テンプレートではない単一のoperator()があり、これは実行可能です-実際に&X::operator()を検査し、それらの署名を操作して、必要な新しい署名を生成できるためです。これにより、コンパイラを使用して過負荷解決を行うことができます。

overload(declval<A>(), declval<B>(), ...)(args...)がどのタイプを生成するかを確認することもできます。最適な一致の戻り値の型がほぼすべての実行可能な候補で一意である場合でも、_ref_overload_で正しいオーバーロードを選択できます。オーバーロードまたはテンプレート化された呼び出し演算子を使用していくつかのケースを正しく処理できるようになったため、これにより多くの根拠がカバーされますが、あいまいではない多くの呼び出しを誤って拒否します。


しかし、同じ戻り値の型を持つオーバーロードまたはテンプレート化された呼び出し演算子を持つ型の一般的な問題を解決するには、さらに何かが必要です。将来の言語機能が必要です。

完全に反映すると、上記のように戻り値の型を挿入できます。具体的にどのようになるかはわかりませんが、Yakkによる実装を楽しみにしています。

代替の潜在的な将来の解決策は、オーバーロードされた _operator ._ を使用することです。セクション4.12には、設計がさまざまなoperator.() sを介して名前でさまざまなメンバー関数のオーバーロードを許可することを示す例が含まれています。その提案が今日何らかの同様の形式で合格した場合、参照オーバーロードの実装は、今日のオブジェクトオーバーロードと同じパターンに従い、今日の異なるoperator .() sを異なるoperator () sに置き換えるだけです。

_template <class T>
struct ref_overload_one {
    T& operator.() { return r; }
    T& r;
};

template <class... Ts>
struct ref_overloader : ref_overload_one<Ts>...
{
    ref_overloader(Ts&... ts)
    : ref_overload_one<Ts>{ts}...
    { }

    using ref_overload_one<Ts>::operator....; // intriguing syntax?
};
_
10
Barry