9

ユーザー定義型の関数を実装するためのベスト プラクティスに関する微妙な質問に対するこの魅力的な回答を研究しています。(私の質問は、当初、名前空間に型を追加することの違法性に関する議論によって動機付けられました。)swapstd

上記のリンクの回答からコード スニペットをここに再印刷しません。

代わりに、答えを理解したいと思います。

上記でリンクした回答は、最初のコード スニペットの下に、(その名前空間に特化するのではなく) オーバーロードに関して次のように述べますswapnamespace std

コンパイラが別のものを出力する場合、テンプレートの「2 フェーズ ルックアップ」が正しく実装されていません。

答えは、オーバーロードするのではなく)特殊swapnamespace std化すると異なる結果(特殊化の場合の望ましい結果)が生成されることを指摘します。

ただし、答えは追加のケースに進みます。ユーザー定義のテンプレートクラスのスワップを特殊化します。この場合も、目的の結果は得られません。

残念ながら、答えは単に事実を述べているだけです。理由は説明されていません。

誰かがその回答について詳しく説明し、その回答で提供されている2つの特定のコードスニペットでルックアップのプロセスを説明してください:

  • ユーザー定義の非テンプレート クラスのオーバーロード(リンクswapnamespace stdれた回答の最初のコード スニペットのように)

  • ユーザー定義のテンプレートクラスに特化swapnamespace stdています(リンクされた回答の最後のコードスニペットのように)

std::swapどちらの場合も、 user-defined ではなくジェネリックが呼び出されswapます。なんで?

(これにより、2 フェーズ ルックアップの性質と、ユーザー定義の実装のベスト プラクティスswapの理由が明らかになります。感謝します。)

4

2 に答える 2

8

標準語をふんだんに使ったプリアンブル

この例の への呼び出しにswap()は、その引数begin[0]と周囲の関数テンプレートbegin[1]のテンプレート パラメーターに依存するため、従属名が必要です。このような従属名の 2 段階の名前検索は、標準で次のように定義されています。Talgorithm()

14.6.4.2 候補関数 [temp.dep.candidate]

1 後置式が従属名である関数呼び出しの場合、次の点を除いて、通常のルックアップ規則 (3.4.1、3.4.2) を使用して候補関数が検出されます。

— 非修飾名ルックアップ (3.4.1) を使用したルックアップの部分では、テンプレート定義コンテキストからの関数宣言のみが検出されます。

— 関連付けられた名前空間 (3.4.2) を使用したルックアップの部分では、テンプレート定義コンテキストまたはテンプレート インスタンス化コンテキストのいずれかにある関数宣言のみが検出されます。

非修飾ルックアップは、によって定義されます

3.4.1 非修飾名ルックアップ [basic.lookup.unqual]

1 3.4.1 にリストされているすべてのケースで、スコープは、それぞれのカテゴリのそれぞれにリストされている順序で宣言を検索されます。 名前の検索は、名前の宣言が見つかるとすぐに終了します。宣言が見つからない場合、プログラムは不正な形式です。

および引数依存ルックアップ (ADL) として

3.4.2 引数依存の名前検索 [basic.lookup.argdep]

1 関数呼び出し (5.2.2) の postfix-expression が unqualified -idの場合、通常の非修飾ルックアップ (3.4.1) では考慮されない他の名前空間が検索される可能性があり、それらの名前空間では、名前空間スコープのフレンド関数または他の方法では表示されない関数テンプレート宣言 (11.3) が見つかる場合があります。検索に対するこれらの変更は、引数のタイプ(およびテンプレート テンプレート引数の場合は、テンプレート引数の名前空間)によって異なります。

例への標準の適用

最初のは を呼び出しますexp::swap()。これは従属名ではなく、2 フェーズの名前ルックアップを必要としません。swap(T&, T&)swap の呼び出しは修飾されているため、一般的な関数テンプレートのみを検索する通常のルックアップが行われます。

2 番目の(@HowardHinnant が「最新のソリューション」と呼んでいるもの) は、呼び出し元と同じ名前空間(この場合はグローバル名前空間) にswap()オーバーロードがあります。swap の呼び出しは修飾されていないため、通常のルックアップと ADL の両方が定義の時点で行われますが (ここでも generic を見つけるだけです)、インスタンス化の時点で別の ADL が行われます (つまり、が呼び出されている場所) 。オーバーロードの解決中はより適切に一致します。swap(A&, A&)class Aswap(T&, T&)exp::algorithm()main()swap(A&, A&)

ここまでは順調ですね。アンコール: 3 番目の例では、 が呼び出され、内部swap()に特殊化が含まれています。ルックアップは 2 番目の例と同じですが、ADL はテンプレートの特殊化を取得しません。関連付けられた名前空間にないためです。ただし、特殊化はオーバーロードの解決中には役割を果たしませんが、使用時にインスタンス化されます。template<> swap(A&, A&)namespace expclass Atemplate<> swap(A&, A&)

最後に、4 番目の例では、グローバル名前空間に存在するためswap()のオーバーロードを呼び出し、template<class T> swap(A<T>&, A<T>&)内部namespace expに持っています。ルックアップは 3 番目の例と同じですが、クラス template の関連付けられた名前空間にないためtemplate<class T> class A、ADL はオーバーロードを取得しません。この場合、使用時にインスタンス化する必要がある特殊化も存在しないため、ジェネリックがここで呼び出されます。swap(A<T>&, A<T>&)A<T>swap(T&, T&)

結論

に新しいオーバーロードを追加することは許可されておらずnamespace std、明示的な特殊化のみを追加することは許可されていませんが、2 フェーズの名前検索のさまざまな複雑さのために機能しません。

于 2014-01-27T15:41:31.237 に答える
4

ユーザー定義型をオーバーロードswapすることはできません。namespace stdはじめに (特殊化とは対照的に) のオーバーロードnamespace stdは未定義の動作です (標準では違法であり、診断は不要です)。

一般に、関数をクラスに特化することは不可能です(クラスインスタンスtemplateとは対照的に、つまり、はインスタンスですが、クラス全体です)。特殊化のように見えるものは、実際にはオーバーロードです。したがって、最初の段落が適用されます。templatestd::vector<int>std::vector<T>template

ユーザー定義を実装するためのベスト プラクティスは、関数またはオーバーロードを自分のまたは住んでいる名前空間と同じ名前空間swapに導入することです。swaptemplateclass

次に、swap適切なコンテキスト ( using std::swap; swap(a,b);) で呼び出された場合 (ライブラリで呼び出される方法) std、ADL が開始され、オーバーロードが検出されます。

もう 1 つのオプションは、特定の型に対してswapinを完全に特殊化することです。存在するクラスのすべてのインスタンスごとに特化する必要があるため、stdこれはクラスでは不可能 (または非現実的) です。他のクラスの場合、特殊化はその特定のタイプにのみ適用されるため、脆弱です。サブクラスも同様に再特殊化する必要があります。templatetemplatestd

一般に、関数の特殊化は非常に脆弱であり、オーバーライドを導入する方が適切です。オーバーライドを に導入することはできないため、オーバーライドstdが確実に見つかる唯一の場所は独自のnamespaceです。独自の名前空間でのそのようなオーバーライドは、同様にオーバーライドよりも優先さstdれます。

swap名前空間にa を挿入する方法は 2 つあります。どちらもこの目的のために機能します。

namespace test {
  struct A {};
  struct B {};
  void swap(A&, A&) { std::cout << "swap(A&,A&)\n"; }
  struct C {
    friend void swap(C&, C&) { std::cout << "swap(C&, C&)\n"; }
  };

  void bob() {
    using std::swap;
    test::A a, b;
    swap(a,b);
    test::B x, y;
    swap(x, y);
    C u, v;
    swap(u, v);
  }
}

void foo() {
  using std::swap;
  test::A a, b;
  swap(a,b);
  test::B x, y;
  swap(x, y);
  test::C u, v;
  swap(u, v);

  test::bob();
}
int main() {
  foo();
  return 0;
}

1 つ目は直接に注入することnamespaceで、2 つ目は inline として含めることfriendです。「外部演算子」のインラインfriendは、基本的に ADL を介してのみ見つけることができることを意味する一般的なパターンですswapが、この特定のコンテキストではあまり追加されません。

于 2014-01-27T16:28:49.363 に答える