24

別の質問を読んでいるときに、半順序の問題が発生しました。これを次のテストケースに切り詰めました。

template<typename T>
struct Const { typedef void type; };

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

int main() {
  // GCC chokes on f(0, 0) (not being able to match against T1)
  void *p = 0;
  f(0, p);
}

どちらの関数テンプレートでも、オーバーロード解決に入る特殊化の関数タイプはですvoid(int, void*)。しかし、半順序(comeauとGCCによる)は、2番目のテンプレートがより特殊化されていることを示しています。しかし、なぜ?

半順序を見て、質問がある場所を示しましょう。に従ってQ半順序を決定するために使用される一意の構成タイプである可能性があり14.5.5.2ます。

  • T1(Qが挿入された)の変換されたパラメータリスト: (Q, typename Const<Q>::type*)。引数のタイプは次のとおりですAT=(Q, void*)
  • T2(Qが挿入された)の変換されたパラメータリスト: BT= (Q, void*)、これは引数のタイプでもあります。
  • 変換されていないパラメータリストT1(T, typename Const<T>::type*)
  • 変換されていないパラメータリストT2(T, void*)

C ++ 03はこれを過小評価しているので、私はいくつかの欠陥レポートで読んだ意図を使用しました。上記のT1(私が呼び出した)の変換されたパラメータリストは、 「関数呼び出しからテンプレート引数を推測する」ATの引数リストとして使用されます。14.8.2.1

14.8.2.1AT変換する必要も、それ自体を変換する必要もありBTません(たとえば、参照宣言子を削除するなど)。これは、 /ペア14.8.2.4ごとに独立して型演繹を行います。AP

  • ATに対してT2:。ここでの唯一のテンプレートパラメータであり、それはである必要があります。型の演繹は、に対して自明に成功します。{ (Q, T), (void*, void*) }TTQATT2

  • BTに対してT1:。それもここにあります。は推論されていないコンテキストであるため、何も推論するために使用されることはありません。{ (Q, T), (void*, typename Const<T>::type*) }TQtypename Const<T>::type*


これが私の最初の質問です:これTは最初のパラメータに推定された値を使用しますか?答えが「いいえ」の場合、最初のテンプレートはより特殊化されています。GCCとComeauの両方が、2番目のテンプレートはより特殊化されていると言っており、それらが間違っているとは思わないため、これは当てはまりません。したがって、「はい」と想定し、に挿入void*Tます。段落(14.8.2.4)には、「推論はペアごとに個別に実行され、結果が結合されます」および「ただし、特定のコンテキストでは、値は型の推論に関与せず、代わりに推定されたテンプレート引数の値を使用します」と記載されています。他の場所または明示的に指定されています。」これも「はい」のように聞こえます。

したがって、すべてのA / Pペアについて、控除も成功します。現在、各テンプレートは少なくとも他のテンプレートと同じように特殊化されています。これは、控除が暗黙の変換にも依存せず、両方向で成功したためです。結果として、呼び出しはあいまいになるはずです。

それで私の2番目の質問:では、なぜ実装は2番目のテンプレートがより専門的であると言うのですか?どの点を見落としましたか?


編集:明示的な特殊化とインスタンス化をテストしましたが、最近のGCCバージョン(4.4)では、特殊化への参照があいまいであると教えてくれましたが、古いバージョンのGCC(4.1)ではそのあいまいさのエラーは発生しません。これは、最近のGCCバージョンでは、関数テンプレートの半順序に一貫性がないことを示しています。

template<typename T>
struct Const { typedef void type; };

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

template<> void f(int, void*) { }
  // main.cpp:11: error: ambiguous template specialization 
  // 'f<>' for 'void f(int, void*)'
4

4 に答える 4

6

これが私の行き先です。間違ったステップは から に進むことであるというチャールズ・ベイリーに同意しますConst<Q>::Type*void*

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

実行したい手順は次のとおりです。

14.5.5.2/2

オーバーロードされた 2 つの関数テンプレートが与えられた場合、一方が他方よりも特殊化されているかどうかは、各テンプレートを順番に変換し、引数推定 (14.8.2) を使用して他方と比較することで判断できます。

14.5.5.2/3-b1

型テンプレート パラメーターごとに一意の型を合成し、関数パラメーター リストでそのパラメーターが出現するたびに、またはテンプレート変換関数を戻り値の型で置き換えます。

私の意見では、型は次のように合成されます。

(Q, Const<Q>::Type*)    // Q1
(Q, void*)              // Q2

の 2 番目の合成パラメーターが であることを要求する文言は見当たりませT1void*。他の文脈での前例も知りません。型Const<Q>::Type*は、C++ 型システム内で完全に有効な型です。

したがって、次の控除手順を実行します。

Q2~T1

T1 のテンプレート パラメーターを推測しようとするため、次のようになります。

  • パラメーター 1: Tであると推定されますQ
  • パラメータ 2: 非推定コンテキスト

パラメーター 2 は推定されていないコンテキストですが、T の値があるため、推定は成功しています。

Q1~T2

T2 のテンプレート パラメーターを推定すると、次のようになります。

  • パラメーター 1: Tであると推定されますQ
  • パラメータ 2: void*一致しないConst<Q>::Type*ため、推定に失敗します。

私見、ここで標準が私たちを失望させます。パラメーターは依存していないため、何が起こるかは明確ではありませんが、私の経験 (14.8.2.1/3 の目を凝らした読み取りに基づく) では、パラメーターの型 P が依存していない場合でも、引数の型 A は一致する必要があります。それ。

T1 の合成された引数を使用して T2 を特殊化できますが、その逆はできません。したがって、T2 は T1 よりも特殊化されており、最適な機能です。


更新 1:

無効であることについてのポージングをカバーするためだけにConst<Q>::type。次の例を検討してください。

template<typename T>
struct Const;

template<typename T>
void f(T, typename Const<T>::type*) // T1
{ typedef typename T::TYPE1 TYPE; }

template<typename T>
void f(T, void*)                    // T2
{ typedef typename T::TYPE2 TYPE ; }

template<>
struct Const <int>
{
  typedef void type;
};

template<>
struct Const <long>
{
  typedef long type;
};

void bar ()
{
  void * p = 0;
  f (0, p);
}

上記でConst<int>::typeは、通常のオーバーロード解決ルールを実行するときに使用されますが、部分的なオーバーロード ルールに到達するときは使用されません。の任意の特殊化を選択するのは正しくありませんConst<Q>::typeConst<Q>::type*直感的ではないかもしれませんが、コンパイラはフォームの合成された型を持ち、型推定中にそれを使用することに非常に満足しています。


更新 2

template <typename T, int I>
class Const
{
public:
  typedef typename Const<T, I-1>::type type;
};

template <typename T>
class Const <T, 0>
{
public:
  typedef void type;
};

template<typename T, int I>
void f(T (&)[I], typename Const<T, I>::type*)     // T1
{ typedef typename T::TYPE1 TYPE; }

template<typename T, int I>
void f(T (&)[I], void*)                           // T2
{ typedef typename T::TYPE2 TYPE ; }


void bar ()
{
  int array[10];
  void * p = 0;
  f (array, p);
}

Constテンプレートが何らかの値でインスタンス化されると、テンプレートは 0 になるIまで再帰的にインスタンス化されます。Iこれは、部分的な特殊化Const<T,0>が選択されたときです。関数のパラメーターの実数型を合成するコンパイラーがある場合、コンパイラーは配列インデックスにどの値を選択するでしょうか? 10と言いますか?これは上記の例では問題ありませんが、部分的な特殊化とは一致しませんConst<T, 10 + 1>。少なくとも概念的には、プライマリの再帰的なインスタンス化が無限に発生することになります。選択した値が何であれ、終了条件をその値 + 1 になるように変更すると、半順序付けアルゴリズムで無限ループが発生します。

部分順序付けアルゴリズムがどのように正しくインスタンス化され、実際のConstものを見つけることができるかわかりませtypeん。

于 2009-07-28T11:06:49.913 に答える
3

編集: Clang の部分順序付けアルゴリズムの実装 (Doug Gregor による) を研究した後、元の例があいまいになることを「意図」していないという残りのポスターに同意するようになりました-標準はそれほど明確ではありませんがそのような状況で何が起こるべきかについてかもしれません。この投稿を編集して、修正した考えを示しました(私自身の利益と参考のために)。特に、Clang のアルゴリズムtypename Const<T>::typeは、部分順序付けステップ中に ' ' が 'void' に変換されないこと、および各 A/P ペアが互いに独立して推定されることを明確にしました。

最初は、なぜ以下が曖昧であると考えられたのか疑問に思いました。

        template<class T> void f(T,T*);  // 1

        template<class T> void f(T, int*); // 2

        f(0, (int*)0); // ambiguous

(The above is ambiguous because one cannot deduce f1(U1,U1*) from f2(T,int*), and going the other way, one cannot deduce f2(U2,int*) from f1(T,T*). Neither is more specialized.)

しかし、以下はあいまいではありません。

        template<class T> struct X { typedef int type; };
        template<class T> void f(T, typename X<T>::type*); // 3
        template<class T> void f(T, int*); // 2

(あいまいであると予想される理由は、次のような場合です:
- f3(U1,X<U1>::type*) -> f3(U1, int*) ==> f2(T,int*) (deduction ok, T=U1)
-f2(U2,int*) ==> f3(T, X<T>::type*) (deduction ok, T=U2 makes X<U2>::type* -> int*)
もしこれが本当なら、どちらも他より専門化されていないでしょう。)

Clang の半順序付けアルゴリズムを調べたところ、上記の '3' を次のように扱っていることが明らかです。

template<class T, class S> void f(T, S*); // 4

したがって、「typename X::type」に対する一意の「U」の推定は成功します -

  • f3(U1,X<U1>::type*) is treated as f3(U1, U2*) ==> f2(T,int*) (deduction not ok)
  • f2(U2,int*) ==> f3(T,S* [[X<T>::type*]]) (deduction ok, T=U2, S=int)

したがって、「2」は明らかに「3」よりも専門的です。

于 2009-07-25T18:30:16.400 に答える
1

編集:この投稿を無視してください-Doug Gregorによって実装された半順序のclangsアルゴリズムを研究した後(この記事の執筆時点では部分的にしか実装されていませんが-OPの質問に関連するロジックは十分に実装されているようです)-それ推論されていないコンテキストを単なる別のテンプレートパラメータとして扱うように見えます。これは、明示的なvoid *引数を使用したオーバーロードは、より特殊なバージョンであり、あいまいさがないことを示しています。いつものようにコモーは正しいです。さて、この振る舞いを明確に定義する規格の文言については、それは別の問題です...

この投稿はcomp.lang.c++。moderatedにも投稿されており、そこでも混乱を引き起こしているようです-議論は明らかにここで尋ねられた質問に関連しているので、私はそのグループへの私の答えもここに投稿すると思いました- 。

On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:

You are going one step too fast here. How do you know (and would the compiler know) that there is no specialisation of Const<Q> such that Const<Q>::type != void?

As far as I can see, the compiler would transform the parameter-list of A to: AT=(Q, <unknown>*). To call B with these parameters requires an implicit conversion (<unknown>* to void*) and therefore A is less specialised than B.

これは間違っていると思います。どの関数がより特殊化されているかを確認するとき(半順序付け中)、コンパイラはパラメータリストを次のように変換します(Q, void*)-つまり、実際に関連するテンプレートをインスタンス化し(最適な一致)、その中を「type」の値を探します-これで場合によっては、プライマリテンプレートに基づいて、無効になります*。

部分的な特殊化に関するあなたのポイントに関して-どのテンプレートが他のテンプレートよりも特殊化されているかをチェックするとき、使用できる唯一のタイプは一意に生成されたタイプです-宣言のインスタンス化の時点で他の特殊化がある場合(過負荷解決が行われている)それらは考慮されます。後でそれらを追加し、それらが選択される必要がある場合は、ODRに違反することになります(14.7.4.1による)

部分的/明示的な特殊化も候補セットの形成中に考慮されますが、今回は関数への実際の引数のタイプを使用します。(Xの)最も一致する部分特殊化の結果、一部のパラメーターに対してより適切な暗黙の変換シーケンスを持つ関数型が得られた場合、半順序フェーズに到達することはなく、その「より適切な」関数が(作成する前に)選択されます。半順序フェーズに移行します)

これは、さまざまなステップで何が行われるべきかについてのコメント付きの例です。

    template<class T, bool=true> struct X;  // Primary

    template<class T> struct X<T,true> { typedef T type; };  // A
    template<> struct X<int*,true> { typedef void* type; };  // B


    template<class T> void f(T,typename X<T>::type); //1
    template<class T> void f(T*,void*); //2


    int main()
    {
      void* pv;
      int* pi;


      f(pi,pi);   
      // two candidate functions: f1<int*>(int*,void*),  f2<int>(int*,void*)
      // Note: specialization 'B' used to arrive at void* in f1
      // neither has a better ICS than the other, so lets partially order
      // transformed f1 is f1<U1>(U1,X<U1,true>::type) --> f1<U1>(U1,U1) 
      //       (template 'A' used to get the second U1)
      // obviously deduction will fail (U1,U1) -> (T*,void*)
      // and also fails the other way (U2*, void*) -> (T,X<T>::type)
      // can not partially order them - so ambiguity 




      f(pv,pv);  
      // two candidate functions: f1<void*>(void*,void*), f2<void>(void*,void*)
      // Note: specialization 'A' used to arrive at second void* in f1
      // neither has a better ICS than the other, so lets partially order
      // transformed f1 is f1<U1>(U1,X<U1>::type) --> f1<U1>(U1,U1) 
      //       (template 'A' used to get the second U1)
      // obviously deduction will fail (U1,U1) -> (T*,void*)
      // and also fails the other way (U2*, void*) -> (T,X<T>::type)
      // can not partially order them - so ambiguity again             

    }

また、プライマリテンプレートに定義がない場合、SFINAEは半順序フェーズで動作し、どちらも他方から推測できず、あいまいさが生じるはずです。

また、これらの関数のいずれかのインスタンス化のポイントが翻訳ユニットの他の場所に移動された場合に別の一致につながる別のテンプレートを追加すると、明らかにODRに違反します。

On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:

まず、より専門的であるということは、過負荷解決によってそのテンプレートを選択できるタイプが少ないことを意味します。これを使用して、半順序の規則を次のように要約できます。Aは呼び出せるが、Bは呼び出せない、または過負荷解決がAを呼び出すことを好むようなAの型を見つけようとします。その型が見つかった場合、Bはより特殊化されます。 Aより。

ここでは議論はありません。しかし、現在のルールに基づくと、OPの例はあいまいである必要があります。


最後に、litbによって提起された2つの特定の質問に対する明確で明確な回答を次に示します。

1)これで、最初のパラメーターとして推定されたTの値が使用されますか?
はい-もちろん、それはテンプレート引数の推論を行っている必要があります-「リンク」を維持する必要があります。

2)さて、なぜ実装は2番目が代わりにより専門化されていると言うのですか?
彼らは間違っているので;)

これで問題が解決することを願っています-まだ不明な点があれば教えてください:)

編集:litbは彼のコメントで良い点を挙げました-おそらく、プライマリテンプレートは常に一意の生成されたタイプでインスタンス化に使用されると述べているのは強すぎるステートメントです。
プライマリテンプレートが呼び出されない場合があります。
私が得ているのは、半順序が発生しているときに、いくつかの一意の生成されたタイプが最適な専門分野に一致するために使用されるということです。そうです、それはプライマリテンプレートである必要はありません。私はそうするために上記の言語を編集しました。彼はまた、インスタンス化の時点の後で、より一致するテンプレートを定義することに関する問題を提起しました。インスタンス化のポイントに関するセクションによると、これはODRの違反になります。


標準では、A / Pペアが作成されると(temp.func.orderで説明されている変換ルールを使用して)、テンプレート引数の推論(temp.deduct)を使用して相互に推定され、そのセクションで次のケースが処理されます。推論されていないコンテキスト、テンプレートとそのネストされたタイプのインスタンス化、インスタンス化のポイントのトリガー。temp.pointセクションはODR違反を処理します(半順序の意味は、変換ユニット内のインスタンス化のポイントに関係なく変更されるべきではありません)。混乱がどこから来ているのかまだわかりませんか?–ファイサルバリ1時間前[このコメントを削除]

litb:"引数を作成するためにQをConst:: typeに入れるステップは、SFINAEルールによって明示的にカバーされていないことに注意してください。SFINAEルールは引数の演繹で機能します。Qを関数テンプレート関数パラメーターリストに入れる段落は次のとおりです。 14.5.5.2。 '

ここではSFINAEルールを使用する必要があります-どうして使用できないのでしょうか?私はそれが十分に暗示されていると感じています-私はそれがより明確になる可能性があることを否定しません、そして私は委員会にこれを明確にすることを勧めます-私はあなたの例を十分に解釈するために明確にする必要はないと思います。

それらをリンクする1つの方法を提供しましょう。(14.8.2)から:「明示的なテンプレート引数リストが指定されている場合、テンプレート引数はテンプレートパラメータリストと互換性があり、以下に説明する有効な関数型になる必要があります。そうでない場合、型の推定は失敗します。」

From(14.5.5.2/3) "使用される変換は次のとおりです。—タイプテンプレートパラメーターごとに、一意のタイプを合成し、関数パラメーターリスト内のそのパラメーターの出現ごとに、またはテンプレート変換関数の代わりに、戻り値を返します。タイプ。"

私の考えでは、上記の引用は、テンプレートパラメータごとに一意の生成型を「作成」したら、関数テンプレートに一意の型をテンプレート引数として明示的に指定することにより、関数宣言を暗黙的にインスタンス化する必要があることを意味します。これにより無効な関数型が生成される場合、変換だけでなく、さらに重要なことに、関数を半順序にするために必要な後続のテンプレート引数の推定が失敗します。

From(14.5.5.2/4) "変換された関数パラメーターリストを使用して、他の関数テンプレートに対して引数の推論を実行します。変換されたテンプレートは、推論が成功し、推定されたパラメータータイプが成功した場合にのみ、少なくとも他のテンプレートと同じように特殊化されます。完全に一致します(したがって、控除は暗黙の変換に依存しません)。」

変換された関数パラメーターリストが置換の失敗につながる場合、推論は成功しなかった可能性があります。そして、控除が成功しなかったので、それは他のものほど専門的ではありません-それは私たちが2つを半順序で進めるために知る必要があるすべてです。

litb:この場合に何が起こるかもわかりません:template<typename T> struct A; template<typename T> void f(T, typename A<T>::type); template<typename T> void f(T*, typename A<T>::type);確かに、それは有効なコードであることが意図されていますが、A :: typeを実行すると、テンプレート定義コンテキストでAがまだ定義されていないため、失敗します。順序を決定しようとしているときに、この種の置換から生じるテンプレートのインスタンス化に対して定義されたPOIはありません(半順序はコンテキストに依存しません。これは、関連する2つの関数テンプレートの静的プロパティです)。これは、修正が必要な標準。

わかりました-私は私たちが物事を異なって見ているところを見ていると思います。私があなたを正しく理解しているなら、あなたはこれらの関数テンプレートが宣言されると、コンパイラはそれらの間で選択するためにトリガーされる過負荷解決に関係なく、それらの間の半順序を追跡していると言っています。それがあなたがそれを解釈する方法であるならば、私はあなたがあなたが説明する上記の振る舞いを期待する理由を見ることができます。しかし、私は、規格がこれを要求または義務付けているとは思いません。

現在、標準では、半順序が関数の呼び出しに使用される型に依存しないことが明確になっています(これは、静的プロパティとして記述し、コンテキストに依存しない場合に参照しているものだと思います)。

また、この標準は、ICSに基づいてより適切な関数を選択できなかった場合、または1つである場合にのみ、過負荷解決(13.3.3 / 1)のプロセス中に関数テンプレート間の半順序(半順序を呼び出す)のみを考慮していることも明確です。はテンプレートであり、もう一方はそうではありません。[クラステンプレートの半順序化は別の問題であり、私の考えでは、その特定のクラスのインスタンス化を必要とする関連するコンテキスト(他のテンプレート定義)を使用します。]

したがって、私の意見では、関数テンプレートの半順序の機構は、オーバーロード解決が実行されるときに呼び出されるため、オーバーロード解決が実行されている時点で利用可能なコンテキストの関連部分(テンプレート定義と特殊化)を使用する必要があります。

したがって、私の解釈に基づいて、上記の「template struct A」を使用した例によれば、コードは有効です。半順序は、定義コンテキストでは実行されません。しかし、f((int *)0,0)の呼び出しを記述して、2つの関数間でオーバーロード解決を呼び出した場合、およびその時点で、コンパイラが候補宣言をアセンブルしようとするか、それらを半順序化しようとします(if関数型の一部として無効な式または型が生成された場合、SFINAEは私たちを助け、テンプレートの推定が失敗することを通知します(半順序に関する限り、これ以上のことはできないことを意味します)。テンプレートを変換することさえできなかった場合は、他のものよりも専門的です)。

POIに関しては、私がそうであるように、変換された関数型は、明示的に指定されたテンプレート引数リストを使用して(一意に生成された型を使用して)暗黙のインスタンス化を表すことになっていると確信している場合は、次の標準引用符が適切です。

14.6.4.1 / 1関数テンプレートの特殊化、メンバー関数テンプレートの特殊化、またはクラステンプレートのメンバー関数または静的データメンバーの特殊化の場合、特殊化が別のテンプレートの特殊化および参照元のコンテキストはテンプレートパラメータによって異なります。スペシャライゼーションのインスタンス化のポイントは、それを囲むスペシャライゼーションのインスタンス化のポイントです。

私がこれを解釈する方法は、変換された関数型と元の関数型のPOIは、実際の関数呼び出しによって作成された関数のPOIと同じであるということです。

litb:半順序はむしろ a property of the syntactic form of parameters (i.e "T*" against "T(*)[N]"), 私だけなので、仕様の修正に投票します(「Qが、型に名前を付ける修飾IDのネストされた名前指定子に表示される場合、名前が付けられた型は「Q」です)。は別のユニークなタイプです This means that in template<typename T> void f(T, typename Const<T>::type*); the argument list is (Q, R*), for example. Same for template<typename T> void f(T*, typename ConstI<sizeof(T)>::type); the arg lisst would be (Q*, R). A similar rule would be needed for non-type parameters, of course. が、これが自然な順序になるかどうかを確認するために、それについて考え、いくつかのテストケースを作成する必要があります。

ああ-今、あなたは私たち全員が直感的に期待するものを支持して曖昧さを解決する可能な解決策を提案しています-これは別の問題です、そして私はあなたのようにあなたが向かっている方向が好きですが、私もいくつか考えなければなりませんその作業性を宣言する前にそれに。

議論を続けてくれてありがとう。SOがコメントの配置だけに制限されていなかったらいいのにと思います。

私の投稿は編集できますので、よろしければ投稿内でお気軽にご返信ください。

于 2009-07-26T16:07:51.140 に答える
1

T1 の変換されたパラメーター リスト (Q 挿入): (Q, typename Const::type*)。引数の型は AT = (Q, void*)

それは本当に正しい単純化なのだろうか。type を合成するとき、テンプレートの仕様化の順序を決定する目的でQ特殊化を呼び出すことはできますか?Const

template <>
struct Const<Q> { typedef int type; }

これは、パラメータが特定のテンプレート パラメータの の 2 番目のパラメータと一致しないため、T2が少なくともそれほど特殊化されていないことを意味します。T1void*T1

于 2009-07-24T22:39:42.907 に答える