次のコードはコンパイルできません。
void baz(int i) { }
void baz() { }
class Bar
{
std::function<void()> bazFn;
public:
Bar(std::function<void()> fun = baz) : bazFn(fun){}
};
int main(int argc, char **argv)
{
Bar b;
return 0;
}
std::function
this other post で読んだように、オーバーロードの解決を考慮しないと言われています。
この種のソリューションを強制した技術的な制限を完全には理解していません。
翻訳のフェーズ と テンプレート についてcppreferenceで読みましたが、反例を見つけることができなかった理由は思いつきません。平凡な人(まだC++の新人)に説明すると、何がどの翻訳段階で上記のコンパイルが失敗しますか?
これは実際には「翻訳のフェーズ」とは何の関係もありません。純粋に_std::function
_のコンストラクタについてです。
std::function<R(Args)>
は、与えられた関数がR(Args)
型のexactlyである必要はありません。特に、関数ポインタが与えられている必要はありません。呼び出し可能であれば、呼び出し可能な型(メンバー関数ポインタ、operator()
のオーバーロードを持つオブジェクト)を取ることができますかのようにArgs
パラメータが必要ですそして何かを返します変換可能R
(またはR
がvoid
の場合、何でも返すことができます)。
そのためには、_std::function
_の適切なコンストラクターがtemplate:template<typename F> function(F f);
でなければなりません。つまり、任意の関数タイプを取ることができます(上記の制限に従います)。
式baz
はオーバーロードセットを表します。その式を使用してオーバーロードセットを呼び出す場合は問題ありません。その式を特定の関数ポインターを受け取る関数のパラメーターとして使用すると、C++はオーバーロードセットを1つの呼び出しに絞り込み、問題なく実行できるようになります。
ただし、関数がテンプレートであり、テンプレート引数の推定を使用してそのパラメーターが何であるかを理解している場合、C++には、オーバーロードセット内の正しいオーバーロードを判別する機能がありません。したがって、直接指定する必要があります。
オーバーロードの解決は、(a)関数/演算子の名前を呼び出している場合、または(b)明示的なシグネチャを使用してそれを(関数またはメンバー関数への)ポインターにキャストしている場合にのみ発生します。
ここではどちらも発生していません。
_std::function
_は、署名と互換性のある任意のオブジェクトを取ります。特に関数ポインタを取りません。 (ラムダはstd関数ではなく、std関数はラムダではありません)
私の自作の関数バリアントでは、署名R(Args...)
のために、まさにこの理由でR(*)(Args...)
引数(完全一致)も受け入れます。しかし、これは「完全一致」シグネチャを「互換性のある」シグネチャよりも高くすることを意味します。
中心的な問題は、オーバーロードセットがC++オブジェクトではないことです。オーバーロードセットに名前を付けることはできますが、「自然に」渡すことはできません。
これで、次のような関数の疑似オーバーロードセットを作成できます。
_#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
#define OVERLOADS_OF(...) \
[](auto&&...args) \
RETURNS( __VA_ARGS__(decltype(args)(args)...) )
_
これにより、関数名のオーバーロード解決を実行できる単一のC++オブジェクトが作成されます。
マクロを展開すると、次のようになります。
_[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }
_
これは書くのが面倒です。シンプルで少しだけ有用性の低いバージョンはこちらです:
_[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }
_
任意の数の引数を取り、baz
に完全に転送するラムダがあります。
次に:
_class Bar {
std::function<void()> bazFn;
public:
Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};
_
動作します。オーバーロードの解決は、fun
にオーバーロードセットを直接渡す(解決できない)代わりに、fun
に格納するラムダに遅延させます。
関数名をオーバーロードセットオブジェクトに変換するC++言語の操作を定義する提案が少なくとも1つあります。そのような標準的な提案が標準に含まれるまで、_OVERLOADS_OF
_マクロが役立ちます。
さらに一歩進んで、cast-to-compatible-function-pointerをサポートできます。
_struct baz_overloads {
template<class...Ts>
auto operator()(Ts&&...ts)const
RETURNS( baz(std::forward<Ts>(ts)...) );
template<class R, class...Args>
using fptr = R(*)(Args...);
//TODO: SFINAE-friendly support
template<class R, class...Ts>
operator fptr<R,Ts...>() const {
return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
}
};
_
しかし、それは鈍化し始めています。
実例 。
_#define OVERLOADS_T(...) \
struct { \
template<class...Ts> \
auto operator()(Ts&&...ts)const \
RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
template<class R, class...Args> \
using fptr = R(*)(Args...); \
\
template<class R, class...Ts> \
operator fptr<R,Ts...>() const { \
return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
} \
}
_
ここでの問題は、ポインターの減衰に対する関数の実行方法をコンパイラーに指示しないことです。あなたが持っている場合
void baz(int i) { }
void baz() { }
class Bar
{
void (*bazFn)();
public:
Bar(void(*fun)() = baz) : bazFn(fun){}
};
int main(int argc, char **argv)
{
Bar b;
return 0;
}
次に、コードが機能するようになります。これで、具体的な型が割り当てられているため、コンパイラーは必要な関数を認識します。
std::function
を使用する場合、次の形式の関数オブジェクトコンストラクターを呼び出します
template< class F >
function( F f );
また、テンプレートであるため、渡されるオブジェクトのタイプを推測する必要があります。 baz
はオーバーロードされた関数であるため、推定できる単一の型がないため、テンプレートの推定は失敗し、エラーが発生します。あなたが使用する必要があります
Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}
強制的に単一のタイプを取得し、控除を許可します。
この時点で、コンパイラーはstd::function
コンストラクターに渡すオーバーロードを決定し、std::function
コンストラクターが任意の型を取るようにテンプレート化されていることを知っています。両方のオーバーロードを試し、最初のものがコンパイルされないのに2番目がコンパイルされることを確認する機能はありません。
これを解決するには、static_cast
を使用して、どのオーバーロードが必要かをコンパイラーに明示的に通知します。
Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}