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...
_を参照しますか?
さて、これが計画です。質問に示されているように、継承に基づいて宣言を使用した場合に選択される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
}
_
警告:
ref_overloader
_はその構成関数オブジェクトにアンラップされるため、それらのoperator()
は転送operator()
。 _ref_overloader
_ sを作成しようとする他のオーバーロードは、同様のことを行わない限り、明らかに失敗します。いくつかの便利なビット:
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 。
一般的には、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の場合-A
とB
の両方(および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?
};
_