std :: swap()をオーバーロードする方法


115

std::swap()並べ替えや割り当ての際に、多くのstdコンテナ(std::listおよびなどstd::vector)で使用されます。

ただし、のstd実装swap()は非常に一般化されており、カスタムタイプに対しては非効率的です。

したがってstd::swap()、カスタムタイプ固有の実装でオーバーロードすることで効率を上げることができます。しかし、どのように実装すれば、標準コンテナで使用できますか?



回答:


135

スワップをオーバーロードする正しい方法は、スワップしているのと同じ名前空間にスワップを書き込むことです。これにより、引数依存ルックアップ(ADL)を介して見つけることができます。特に簡単なことは、次のとおりです。

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};

11
C ++ 2003では、せいぜい指定不足です。ほとんどの実装では、ADLを使用してスワップを検出しますが、必須ではないため、期待することはできません。あなたはでき OPによって示されるように、特定の具体的なタイプのためのstd ::スワップを専門。たとえば、その型の派生クラスなど、その特殊化が使用されることを期待しないでください。
Dave Abrahams、

15
実装がまだ正しいスワップを見つけるためにADLを使用しいないことに気付くとは驚きます。これは委員会の古い問題です。実装でスワップの検索にADLを使用しない場合は、バグレポートを提出してください。
ハワードヒンナン、2011

3
@Sascha:最初に、名前空間スコープで関数を定義しています。これは、汎用コードに関係する唯一の定義であるためです。int et。al。メンバー関数を持たない/持たせない、std :: sort et al。無料の機能を使用する必要があります。プロトコルを確立します。第2に、なぜ2つの実装に反対するのかわかりませんが、メンバー以外のスワップを受け入れることができない場合、ほとんどのクラスは非効率的にソートされる運命にあります。オーバーロードルールにより、両方の宣言が確認された場合、修飾なしでswapが呼び出されたときに、より具体的な宣言(この宣言)が選択されます。
デイブアブラハム

5
@ Mozza314:状況によります。std::sortADLを使用して要素を交換するAは、C ++ 03に準拠していませんが、C ++ 11に準拠しています。また、クライアントが慣用的ではないコードを使用する可能性があるという事実に基づいて、なぜ-1と回答するのでしょうか。
JoeG、2011

4
@curiousguy:規格を読むことが規格を読むという単純な問題であるなら、あなたは正しいでしょう:-)。残念ながら、著者の意図は重要です。したがって、ADLを使用できる、または使用する必要があるという当初の意図があった場合、その仕様は不十分です。そうでない場合、それはC ++ 0xの単純で古い互換性のない変更です。そのため、私は「よくても」過小に記述しました。
デイブアブラハムズ2013

70

モッツァ314

以下は、一般的なstd::algorithm呼び出しの効果のシミュレーションでありstd::swap、ユーザーにネームスペースstdでスワップを提供させています。これは実験であるため、このシミュレーションではのnamespace exp代わりにを使用しますnamespace std

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

私にとってこれは印刷されます:

generic exp::swap

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

コンパイラが(C ++ 98/03/11のいずれかに)準拠している場合、私が表示するのと同じ出力が得られます。そしてその場合、あなたが恐れていることはまさに起こります。また、swap名前空間stdexp)に入れても、その発生は止まりませんでした。

Daveと私はどちらも委員会のメンバーであり、この標準の領域で10年間取り組んでいます(常に互いに合意しているわけではありません)。しかし、この問題は長い間解決されており、私たちは両者がどのように解決されたかについて合意します。この領域でのDaveの専門家の意見/回答は無視してください。

この問題は、C ++ 98が公開された後に明らかになりました。2001年頃から、デイブと私はこの分野で働き始めました。そしてこれが最新のソリューションです:

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

出力は次のとおりです。

swap(A, A)

更新

以下のことが観察されました。

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

動作します!それで、なぜそれを使わないのですか?

あなたAがクラステンプレートである場合を考えてみましょう:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

今では再び動作しません。:-(

したがってswap、名前空間stdを入力して機能させることができます。しかし、あなたは置くために覚えておく必要があるでしょうswapAテンプレートを持っている時にケースのための名前空間:A<T>。そして、どちらの場合もあるために動作します、あなたが入れた場合swapAの名前空間、それだけで一つの方法ということ、それを行うことを忘れないでください(とティーチ他人に)するだけで簡単です。


4
詳しい回答ありがとうございました。私は明らかにこれについてあまり知識がなく、実際にどのように過負荷と特殊化が異なる振る舞いを生み出すのか疑問に思っていました。ただし、私はオーバーロードではなく専門化を提案しています。私が入れたときtemplate <>、あなたの最初の例では、私は、出力を得るexp::swap(A, A)GCCから。それで、なぜ専門化を好まないのですか?
voltrevo

1
うわー!これは本当に啓発的です。あなたは間違いなく私を確信しています。私はあなたの提案を少し変更して、Dave Abrahamsのクラス内のフレンド構文を使用すると思います(これはoperator <<にも使用できます!:-))、それを回避する理由がない限り(コンパイル以外)別々に)。また、これに照らしusing std::swapて、「ヘッダーファイル内にステートメントを使用しない」ルールの例外はあると思いますか?実際、using std::swap中に入れてみません<algorithm>か?私はそれが少数の人々のコードを壊すかもしれないと思います。たぶんサポートを廃止し、最終的にそれを入れますか?
voltrevo

3
クラス内のフレンド構文は問題ありません。using std::swapヘッダー内の関数スコープに制限しようと思います。はい、swapほとんどキーワードです。しかし、いいえ、それはまったくキーワードではありません。したがって、本当に必要になるまで、すべての名前空間にエクスポートしないことをお勧めします。 swapのようなものoperator==です。最大の違いは、operator==修飾された名前空間構文で呼び出すことさえ考えたことがないということです(それはあまりに醜いでしょう)。
ハワードHinnant

15
@NielKirk:複雑化とは、間違った答えが多すぎるということです。Dave Abrahamsの正しい答えについて複雑なことは何もありません。「スワップをオーバーロードする正しい方法は、スワップしているのと同じ名前空間にスワップを書き込んで、引数依存ルックアップ(ADL)で見つけられるようにすることです。」
Howard Hinnant 2013年

2
@codeshot:すみません。Herbは1998年以来、このメッセージを得ようと努めてきました:gotw.ca/publications/mill02.htm 彼はこの記事でスワップについて言及していません。しかし、これはハーブのインターフェース原理のもう1つのアプリケーションです。
ハワードヒナント2015

53

(C ++標準により)std :: swapをオーバーロードすることは許可されていませんが、独自の型のテンプレート特殊化をstd名前空間に追加することは特に許可されています。例えば

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

次に、stdコンテナー(およびその他の場所)での使用法は、一般的なものではなく、専門分野を選択します。

また、スワップの基本クラスの実装を提供するだけでは、派生型として十分ではないことにも注意してください。例えばあなたが持っているなら

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

これは基本クラスで機能しますが、2つの派生オブジェクトを交換しようとすると、テンプレートスワップが完全に一致するため、stdの汎用バージョンが使用されます(そして、派生オブジェクトの「基本」部分のみを交換する問題が回避されます)。

注:私はこれを更新して、私の最後の答えから間違ったビットを削除しました。ああ!(指摘してくれたpuetzkとj_random_hackerに感謝)


1
主に良いアドバイスですが、std名前空間でテンプレートを特殊化する(これはC ++標準で許可されています)とオーバーロード(許可されていない)をpuetzkが区別するため、-1にする必要があります。
j_random_hacker 2009年

11
スワップをカスタマイズする正しい方法は自分の名前空間で行うことです(Dave Abrahamsが別の回答で指摘しているため)。
ハワードヒンナン、2011

2
私の反対投票の理由は、ハワードと同じです
デイブアブラハム

13
@ HowardHinnant、Dave Abrahams:同意しない。あなたの代替案は「正しい」方法であると主張する根拠は何ですか?puetzkが標準から引用したように、これは特に許可されています。私がこの問題に慣れていない間は、Fooを定義してスワップすると、私のコードを使用する他の誰かがswap()ではなくstd :: swap(a、b)を使用する可能性が高いため、私はあなたが主張する方法が本当に好きではありません。 a、b)Fooで、非効率的なデフォルトバージョンを静かに使用します。
voltrevo

5
@ Mozza314:コメント領域のスペースとフォーマットの制約により、完全に返信することはできませんでした。私が追加した「注意Mozza314」というタイトルの回答をご覧ください。
ハワードHinnant

29

通常はstd ::名前空間に要素を追加すべきではないのは正しいが、ユーザー定義型のテンプレート特殊化の追加は特に許可されています。関数のオーバーロードはありません。これは微妙な違いです:-)

17.4.3.1/1特に明記されていない限り、C ++プログラムが名前空間stdまたは名前空間stdの名前空間に宣言または定義を追加することは未定義です。プログラムは、標準ライブラリテンプレートのテンプレート特殊化を名前空間stdに追加できます。このような標準ライブラリの特殊化(完全または部分的)は、宣言が外部リンケージのユーザー定義名に依存し、テンプレートの特殊化が元のテンプレートの標準ライブラリ要件を満たさない限り、未定義の動作になります。

std :: swapの特殊化は次のようになります。

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

template <>ビットがないと、許可される特殊化ではなく、未定義のオーバーロードになります。デフォルトの名前空間を変更する@Wilkaの提案するアプローチは、ユーザーコードで機能する可能性があります(名前空間のないバージョンを優先するKoenigルックアップのため)が、これは保証されておらず、実際には実際には想定されていません(STL実装は完全に使用する必要があります) -qualified std :: swap)。

ありcomp.lang.c ++のスレッドが。司会長く話題のdicussion。ただし、そのほとんどは部分的な特殊化に関するものです(現在、これを行うには良い方法はありません)。


7
これ(または何でも)に関数テンプレートの特殊化を使用するのが間違っている1つの理由:オーバーロードと不適切な方法で相互作用します。たとえば、標準のstd :: swapをstd :: vector <mytype>&に特化すると、特化はオーバーロードの解決時に考慮されないため、標準のベクトル固有のスワップよりも特化が選択されません。
Dave Abrahams

4
これは、MeyersがEffective C ++ 3ed(Item 25、pp 106-112)で推奨していることでもあります。
JWW

2
@DaveAbrahams:(明示的なテンプレート引数なしで)特殊化すると、部分的な順序付けによってバージョンが特殊化され使用されます。vector
デイビスヘリング

1
@DavisHerring実際には、その部分的な順序付けは何の役割も果たしません。問題は、それをまったく呼び出せないことではありません。それは明らかに特定のスワップのオーバーロードが存在しない場合に何が起こるかです:wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams

2
@DaveAbrahams:部分的な順序付けは、明示的な特殊化が複数に一致する場合に特殊化する関数テンプレートを選択することです。::swap追加した過負荷がより特化したstd::swapため、過負荷vectorので、呼び出し、後者は関連性があるの無い特殊化をキャプチャし、。それが実際の問題であるかどうかはわかりません(しかし、これが良いアイデアであると主張しているわけでもありません!)。
デイビスヘリング
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.