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


17

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

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私は読んでの通りですが、オーバーロードの解決を考慮することはないと述べ、この他のポスト

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

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


1
@Evgですが、OPの例で受け入れられるのに意味のあるオーバーロードは1つだけです。あなたの例では両方が一致します
idclev 463035818

回答:


13

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

参照してください、std::function<R(Args)>与えられた関数が正確にタイプである必要はありませんR(Args)。特に、関数ポインタが与えられている必要はありません。これは、任意の呼び出し可能なタイプ(メンバ関数ポインタの過負荷を持っているいくつかのオブジェクトを取ることができoperator()、それが呼び出し可能であるとして)かのようにかかったArgsパラメータや戻り何かに変換する R(または場合Rvoid、それが何かを返すことができます)。

これを行うには、適切なコンストラクタがstd::functionある必要がありますテンプレートtemplate<typename F> function(F f);。つまり、任意の関数タイプを取ることができます(上記の制限に従います)。

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

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


何か問題が発生した場合はご容赦ください。C++には、利用可能なオーバーロードを区別する機能がないのはなぜですか。std :: function <T>は互換性のある型を受け入れ、完全一致だけでなく、2つのbaz()オーバーロードのうちの1つだけが、指定されたパラメーターを取得したかのように呼び出すことができることがわかります。曖昧さをなくすことが不可能なのはなぜですか?
TuRtoise

すべてのC ++が見るのは私が引用した署名だからです。それは何にマッチすることになっているのか分かりません。基本的に、C ++コード(テンプレート宣言であるコード)から正しい答えが明確にわからないものに対してオーバーロードセットを使用するときはいつでも、言語はあなたが何を意味するかを説明するように強制します。
Nicol Bolas

1
@TuRtoise:functionクラステンプレートのテンプレートパラメータは無関係です。重要なのは、呼び出すコンストラクターのテンプレートパラメーターです。これは単にtypename F:別名、任意のタイプです。
Nicol Bolas

1
@TuRtoise:あなたは何かを誤解していると思います。テンプレートが機能するので、「F」だけです。シグネチャから、その関数コンストラクターは任意の型をとるので、テンプレート引数演繹でそれを呼び出そうとすると、パラメーターから型が推測されます。オーバーロードセットに控除を適用しようとすると、コンパイルエラーが発生します。コンパイラーは、どの型が機能するかを確認するために、セットからすべての可能な型を推測しようとはしません。
Nicol Bolas

1
「オーバーロードセットに控除を適用しようとすると、コンパイルエラーが発生します。コンパイラは、セットからすべての可能な型を推測して、どれがうまくいくかを確認しようとはしません。」まさに私が欠けていたもの、ありがとうございました:)
TuRtoise

7

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

5

ここでの問題は、ポインターの減衰に対する関数の実行方法をコンパイラーに指示しないことです。あなたが持っている場合

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){}

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


「bazはオーバーロードされた関数であるため、推定できる単一の型はありません。確かではありませんが、あいまいさを解決するのにこれで十分であると私は期待していました
idclev 463035818

3
@ formerlyknownas_463035818しかし、それを決定するには、最初に型を推定する必要があります。オーバーロードされた名前であるため、それはできません。
NathanOliver

1

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

これを解決する方法は、どのオーバーロードが必要かをコンパイラーに明示的に伝えることですstatic_cast

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.