web-dev-qa-db-ja.com

std :: functionがオーバーロードの解決に参加しないのはなぜですか?

次のコードはコンパイルできません。

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::functionthis other post で読んだように、オーバーロードの解決を考慮しないと言われています。

この種のソリューションを強制した技術的な制限を完全には理解していません。

翻訳のフェーズテンプレート についてcppreferenceで読みましたが、反例を見つけることができなかった理由は思いつきません。平凡な人(まだC++の新人)に説明すると、何がどの翻訳段階で上記のコンパイルが失敗しますか?

17
TuRtoise

これは実際には「翻訳のフェーズ」とは何の関係もありません。純粋に_std::function_のコンストラクタについてです。

std::function<R(Args)>は、与えられた関数がR(Args)型のexactlyである必要はありません。特に、関数ポインタが与えられている必要はありません。呼び出し可能であれば、呼び出し可能な型(メンバー関数ポインタ、operator()のオーバーロードを持つオブジェクト)を取ることができますかのようにArgsパラメータが必要ですそして何かを返します変換可能R(またはRvoidの場合、何でも返すことができます)。

そのためには、_std::function_の適切なコンストラクターがtemplatetemplate<typename F> function(F f);でなければなりません。つまり、任意の関数タイプを取ることができます(上記の制限に従います)。

bazはオーバーロードセットを表します。その式を使用してオーバーロードセットを呼び出す場合は問題ありません。その式を特定の関数ポインターを受け取る関数のパラメーターとして使用すると、C++はオーバーロードセットを1つの呼び出しに絞り込み、問題なく実行できるようになります。

ただし、関数がテンプレートであり、テンプレート引数の推定を使用してそのパラメーターが何であるかを理解している場合、C++には、オーバーロードセット内の正しいオーバーロードを判別する機能がありません。したがって、直接指定する必要があります。

13
Nicol Bolas

オーバーロードの解決は、(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){}

強制的に単一のタイプを取得し、控除を許可します。

5
NathanOliver

この時点で、コンパイラーはstd::functionコンストラクターに渡すオーバーロードを決定し、std::functionコンストラクターが任意の型を取るようにテンプレート化されていることを知っています。両方のオーバーロードを試し、最初のものがコンパイルされないのに2番目がコンパイルされることを確認する機能はありません。

これを解決するには、static_castを使用して、どのオーバーロードが必要かをコンパイラーに明示的に通知します。

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
1
Alan Birtles