8

次のコードを検討してください。

#include <iostream>

void f(int) { }

void f(int, short) { }

template<typename... Ts> void g(void (*)(Ts...))
{
   std::cout << sizeof...(Ts) << '\n';
}

template<typename T, typename... Ts> void h(void (*)(T, Ts...))
{
   std::cout << sizeof...(Ts) << '\n';
}

int main()
{
   g(f);         // #1
   g<int>(f);    // #2
   h(f);         // #3
   h<int>(f);    // #4
}

意図は、本体の各行をmain()個別に試すことです。私の予想では、4 つの呼び出しはすべてあいまいであり、コンパイラ エラーが発生するだろうということでした。

私はコードをテストしました:

  • Clang 3.6.0 と GCC 4.9.2 の両方で-Wall -Wextra -pedantic -std=c++14( -std=c++1yfor GCC) を使用 - エラー メッセージの文言のわずかな違いを除いて、これらすべてのケースで同じ動作。
  • Visual C++ 2013 Update 4 と Visual C++ 2015 CTP6 - これも同じ動作なので、今後は "MSVC" と呼びます。

Clang と GCC:

  • #1: コンパイラ エラーです。紛らわしいメッセージが表示されます。基本的にはno overload of 'f' matching 'void (*)()'. 何?no-param 宣言はどこから来たのですか?
  • #3: コンパイラ エラー、別の紛らわしいメッセージ: couldn't infer template argument 'T'。そこで失敗する可能性のあるすべてのことの中で、の引数を推測することはT、私が期待する最後のものになるでしょう...
  • #2and #4: エラーも警告もなくコンパイルされ、最初のオーバーロードが選択されます。

4 つのケースすべてで、オーバーロードの 1 つ (いずれか 1 つ) を削除すると、コードは正常にコンパイルされ、残りの関数が選択されます。これは、Clang と GCC の不一致のように見えます: 結局のところ、両方のオーバーロードで別々に推定が成功した場合、ケース#2#4? 両者は完全に一致していませんか?

さて、MSVC:

  • #1#3および#4: コンパイラ エラー、適切なメッセージ: cannot deduce template argument as function argument is ambiguous。今、それが私が話していることです!ちょっと待って...

  • #2: エラーも警告もなくコンパイルされ、最初のオーバーロードが選択されます。2 つのオーバーロードを別々に試してみると、最初の 1 つだけが一致します。2 番目のものはエラーを生成します: cannot convert argument 1 from 'void (*)(int,short)' to 'void (*)(int)'. もうあまり良くありません。

case で何を探しているかを明確にするために#2、これは標準 (N4296、C++14 final 後の最初のドラフト) が [14.8.1p9] で述べていることです。

テンプレート引数推定は、明示的に指定されたテンプレート引数がシーケンスに含まれている場合でも、テンプレート パラメーター パックに対応するテンプレート引数のシーケンスを拡張できます。

この部分は MSVC ではうまく機能しないようで、 の最初のオーバーロードを選択します#2

これまでのところ、MSVC は完全に正しいとは言えませんが、少なくとも比較的一貫しているようです。Clang と GCC はどうなっていますか? それぞれの場合の標準に従った正しい動作は何ですか?

4

1 に答える 1

7

私が知る限り、Clang と GCC は、特にケース#2との場合、動作が直感に反するように見えるかもしれませんが、標準に従って 4 つのケースすべてで正しいです#4

コード サンプルの関数呼び出しの分析には、主に 2 つの手順があります。1 つ目は、テンプレート引数の演繹と代入です。それが完了すると、すべてのテンプレート パラメータが実際の型に置き換えられた特殊化 ( または のいずれgか)の宣言が生成されます。h

次に、2 番目のステップfで、前のステップで構築された実際の関数へのポインター パラメーターに対して のオーバーロードを照合しようとします。[13.4] の規則に従って、最適な一致が選択されます。 - オーバーロードされた関数のアドレス。私たちの場合、オーバーロードの中にテンプレートがないので、これは非常に単純です。

ここで何が起こるかを理解するための重要なポイントは、最初のステップのあいまいさは、プロセス全体が失敗することを必ずしも意味しないということです。

以下の引用は N4296 からのものですが、内容は C++11 以降変更されていません。

[14.8.2.1p6] は、関数パラメーターが関数へのポインターである場合のテンプレート引数演繹のプロセスを説明しています (私の強調):

P が関数型、関数型へのポインター、またはメンバー関数型へのポインター
の場合: — 引数が 1 つ以上の関数テンプレートを含むオーバーロード セットである場合、パラメーターは推定されないコンテキストとして扱われます。
— 引数がオーバーロード セット (関数テンプレートを含まない) の場合、セットの各メンバーを使用して試行引数推定が試行されます。オーバーロード セット メンバーの 1 つだけで演繹が成功した場合、そのメンバーが演繹の引数値として使用されます。オーバーロード セットの複数のメンバーに対して推定が成功した場合、パラメーターは非推定 context として扱われます

完全を期すために、[14.8.2.5p5] は、一致がない場合でも同じルールが適用されることを明確にしています。

[...]
— 関連する関数引数が関数、またはオーバーロードされた関数のセット (13.4) であり、次の 1 つ以上が適用されるため、引数推定を実行できない関数パラメーター:
— 複数の関数が関数パラメーターの型と一致する (結果があいまいな推定になる)、または
— 関数のパラメーターの型と一致する関数がない、または
— 引数として指定された関数のセットに 1 つ以上の関数テンプレートが含まれている。

したがって、これらの場合のあいまいさによるハードエラーはありません。代わりに、すべてのテンプレート パラメーターは、すべてのケースで推定されないコンテキストにあります。これは [14.8.1p3] と結合します:

[...] 後続のテンプレート パラメーター パック (14.5.3) は、他の方法で推測されない場合、テンプレート引数の空のシーケンスに推測されます。[...]

ここでの「推定」という言葉の使用は混乱を招きますが、これは、どのソースからも要素を推定できず、明示的に指定されたテンプレート引数がない場合、テンプレート パラメーター パックが空のシーケンスに設定されることを意味すると解釈します。 .

これで、Clang と GCC からのエラー メッセージが意味を成し始めます (エラーが発生した理由を理解した後にのみ意味をなすエラーメッセージは、役立つエラー メッセージの正確な定義ではありませんが、何もないよりはましだと思います)。

  • #1:Tsは空のシーケンスなので、gの特殊化のパラメータは確かvoid (*)()にこの場合です。次に、コンパイラは、オーバーロードの 1 つを宛先の型に一致させようとしますが、失敗します。
  • #3:Tは推定されないコンテキストでのみ表示され、明示的に指定されていません (また、パラメーター パックではないため、「空」にすることはできません) h

コンパイルする場合:

  • #2:Tsを推測することはできませんが、1 つのテンプレート パラメータが明示的に指定されているため、Tsint特殊g化のパラメータが作成されますvoid (*)(int)。次に、オーバーロードがこの宛先タイプと照合され、最初のものが選択されます。
  • #4:Tとして明示的に指定されてintおり、Tsは空のシーケンスであるため、hの特殊化のパラメーターはvoid (*)(int)であり、上記と同じです。

オーバーロードの 1 つを削除すると、テンプレート引数の推論中のあいまいさが解消されるため、テンプレート パラメーターは非推定コンテキストになくなり、残りのオーバーロードに従って推定できるようになります。

簡単な検証は、3 番目のオーバーロードを追加することです。

void f() { }

ケース#1のコンパイルを許可します。これは、上記のすべてと一致しています。

このように指定されているのは、関数へのポインター パラメーターに含まれるテンプレート引数を、他の関数引数や明示的に指定されたテンプレート引数など、他のソースから取得できるようにするためだと思います。関数へのポインター パラメーター自体。これにより、関数テンプレートの特殊化宣言をより多くのケースで構築できます。オーバーロードは合成された特殊化のパラメーターと照合されるため、テンプレート引数の推論があいまいな場合でもオーバーロードを選択する方法があることを意味します。これがあなたが求めているものであれば非常に便利ですが、他のいくつかのケースではひどく混乱します - 本当に珍しいことではありません.

面白いのは、MSVC のエラー メッセージは、一見親切で役立つように見えますが、実際には誤解を招くものであり#1、. また、質問で説明されているように、その動作は実装における別の問題の副作用です。そうしないと、同じ誤ったエラー メッセージが表示される可能性があります。#3#4#2#2

これは、Clang と GCC の and のエラー メッセージが好きだと言っているわけではありませ#1#3。少なくとも、推定されていないコンテキストとそれが発生する理由についてのメモを含める必要があると思います。

于 2015-04-02T16:09:58.330 に答える