14

昨夜は眠れず、考え始めましたstd::swap。おなじみのC++98バージョンは次のとおりです。

template <typename T>
void swap(T& a, T& b)
{
    T c(a);
    a = b;
    b = c;
}

ユーザー定義クラスFooが外部リソースを使用する場合、これは非効率的です。一般的なイディオムは、のメソッドvoid Foo::swap(Foo& other)と特殊化を提供することですstd::swap<Foo>。関数テンプレートを部分的に特殊化することはできず、名前空間での名前のオーバーロードは違法であるため、これはクラステンプレートでは機能しないことに注意してください。std解決策は、自分の名前空間にテンプレート関数を記述し、引数に依存するルックアップに依存してそれを見つけることです。これは、クライアントが直接using std::swap呼び出すのではなく、「イディオム」に従うかどうかに大きく依存しますstd::swap。非常にもろい。

C ++ 0xではFoo、ユーザー定義のムーブコンストラクターとムーブ代入演算子がある場合、カスタムswapメソッドとstd::swap<Foo>特殊化を提供してもstd::swap、コピーの代わりに効率的なムーブを使用するため、パフォーマンス上の利点はほとんどありません。

#include <utility>

template <typename T>
void swap(T& a, T& b)
{
    T c(std::move(a));
    a = std::move(b);
    b = std::move(c);
}

もういじる必要がないことswapは、プログラマーからすでに多くの負担を取り除きます。現在のコンパイラーはまだムーブコンストラクターとムーブ代入演算子を自動的に生成しませんが、私が知る限り、これは変更されます。残っている唯一の問題は例外安全性です。これは、一般に、移動操作がスローを許可されており、これによりワームの缶全体が開かれるためです。質問「移動したオブジェクトの状態は正確には何ですか?」物事をさらに複雑にします。

それから私は考えていました、std::swapすべてがうまくいく場合、C ++ 0xのセマンティクスは正確には何ですか?スワップ前後のオブジェクトの状態はどうなっていますか?通常、移動操作によるスワッピングは外部リソースには影響せず、「フラットな」オブジェクト表現自体にのみ影響します。

swapでは、それを正確に実行するテンプレートを単純に作成してみませんか。オブジェクト表現を交換しますか?

#include <cstring>

template <typename T>
void swap(T& a, T& b)
{
    unsigned char c[sizeof(T)];

    memcpy( c, &a, sizeof(T));
    memcpy(&a, &b, sizeof(T));
    memcpy(&b,  c, sizeof(T));
}

これは、それが得るのと同じくらい効率的です:それは単に生のメモリを爆破します。ユーザーの介入は必要ありません。特別なスワップメソッドや移動操作を定義する必要はありません。これは、C ++ 98でも機能することを意味します(右辺値の参照はありません)。しかし、さらに重要なことは、スローが発生しないため、例外安全性の問題を忘れることができるということです。memcpy

このアプローチには2つの潜在的な問題があります。

まず、すべてのオブジェクトが交換されることを意図しているわけではありません。クラスデザイナがコピーコンストラクタまたはコピー代入演算子を非表示にした場合、クラスのオブジェクトを交換しようとすると、コンパイル時に失敗するはずです。タイプでコピーと割り当てが合法であるかどうかをチェックするデッドコードを簡単に紹介できます。

template <typename T>
void swap(T& a, T& b)
{
    if (false)    // dead code, never executed
    {
        T c(a);   // copy-constructible?
        a = b;    // assignable?
    }

    unsigned char c[sizeof(T)];

    std::memcpy( c, &a, sizeof(T));
    std::memcpy(&a, &b, sizeof(T));
    std::memcpy(&b,  c, sizeof(T));
}

どんなまともなコンパイラでも、死んだコードを簡単に取り除くことができます。(「スワップ適合性」をチェックするためのより良い方法はおそらくありますが、それは重要ではありません。重要なのはそれが可能であるということです)。

次に、一部のタイプは、コピーコンストラクターおよびコピー割り当て演算子で「異常な」アクションを実行する場合があります。たとえば、変更をオブザーバーに通知する場合があります。このような種類のオブジェクトは、そもそもコピー操作を提供するべきではなかったので、これは小さな問題だと思います。

このスワッピングのアプローチについてどう思うか教えてください。それは実際に機能しますか?使ってみませんか?これが壊れてしまうライブラリの種類を特定できますか?追加の問題がありますか?議論!

4

5 に答える 5

20

swapでは、それを正確に実行するテンプレートを単純に作成してみませ んか。オブジェクト表現を交換します*。

オブジェクトが構築された後、そのオブジェクトが存在するバイトをコピーすると、オブジェクトが破損する可能性のある方法はたくさんあります。実際、実際には、これが正しく行われない場合は、一見無限の数になります。すべてのケースの98%で機能する可能性があります。

これは、これらすべての根本的な問題は、C以外では、C++ではオブジェクトを単なる生のバイトであるかのように扱ってはならないということです。結局のところ、私たちが構築と破壊を行うのはそのためです。つまり、rawストレージをオブジェクトに変換し、オブジェクトをrawストレージに戻すことです。コンストラクターが実行されると、オブジェクトが存在するメモリは単なるrawストレージではありません。そうでないかのように扱うと、いくつかのタイプが壊れます。

ただし、基本的に、オブジェクトの移動は、アイデアよりもそれほどパフォーマンスが低下することはありません。なぜなら、への呼び出しを再帰的にインライン化し始めると、通常、最終的にはビルトインが移動さstd::move()れる場所に到達するからです。(そして、いくつかのタイプのために移動することがもっとあるなら、あなたはそれらのメモリを自分でいじらないほうがいいです!)確かに、メモリをまとめて移動することは通常、単一の移動よりも高速です(そしてコンパイラがそれが可能であると気付く可能性は低いです個々の動きを1つの包括的なものに最適化します)が、それは私たちが抽象化の不透明なオブジェクトに支払う代償です。そして、特に私たちが以前行っていたコピーと比較すると、それは非常に小さいです。std::memcpy()

ただし、集計タイプswap()の使用std::memcpy()を最適化することはできます。

于 2011-02-02T14:18:24.037 に答える
20

これにより、独自のメンバーへのポインタを持つクラスインスタンスが破損します。例えば:

class SomeClassWithBuffer {
  private:
    enum {
      BUFSIZE = 4096,
    };
    char buffer[BUFSIZE];
    char *currentPos; // meant to point to the current position in the buffer
  public:
    SomeClassWithBuffer();
    SomeClassWithBuffer(const SomeClassWithBuffer &that);
};

SomeClassWithBuffer::SomeClassWithBuffer():
  currentPos(buffer)
{
}

SomeClassWithBuffer::SomeClassWithBuffer(const SomeClassWithBuffer &that)
{
  memcpy(buffer, that.buffer, BUFSIZE);
  currentPos = buffer + (that.currentPos - that.buffer);
}

さて、memcpy()を実行するだけの場合、currentPosはどこを指しますか?明らかに、古い場所に。これにより、各インスタンスが実際に別のインスタンスのバッファを使用するという非常に面白いバグが発生します。

于 2011-02-02T14:20:44.383 に答える
7

一部のタイプは交換できますが、コピーできません。ユニークなスマートポインタがおそらく最良の例です。コピー可能性と割り当て可能性のチェックは間違っています。

TがPODタイプでない場合、memcpyを使用してコピー/移動することは未定義の動作です。


一般的なイディオムは、メソッドvoid Foo :: swap(Foo&other)とstd ::swap<Foo>の特殊化を提供することです。これはクラステンプレートでは機能しないことに注意してください…</p>

より良いイディオムは、非メンバースワップであり、ユーザーがスワップを非修飾で呼び出す必要があるため、ADLが適用されます。これはテンプレートでも機能します。

struct NonTemplate {};
void swap(NonTemplate&, NonTemplate&);

template<class T>
struct Template {
  friend void swap(Template &a, Template &b) {
    using std::swap;
#define S(N) swap(a.N, b.N);
    S(each)
    S(data)
    S(member)
#undef S
  }
};

重要なのは、フォールバックとしてstd::swapのusing宣言です。テンプレートのスワップの友情は、定義を単純化するのに役立ちます。NonTemplateの交換も友だちかもしれませんが、それは実装の詳細です。

于 2011-02-02T14:14:19.960 に答える
6

このような種類のオブジェクトは、そもそもコピー操作を提供するべきではなかったので、これは小さな問題だと思います。

つまり、非常に単純に、間違った負荷です。オブザーバーに通知するクラスとコピーしてはならないクラスは完全に無関係です。shared_ptrはどうですか?明らかにコピー可能である必要がありますが、オブザーバーに参照カウントも明らかに通知します。この場合、スワップ後の参照カウントは同じですが、すべてのタイプに当てはまるわけではなく、マルチスレッドが含まれている場合は特に当てはまりません。代わりに通常のコピーを使用する場合は当てはまりません。スワップなど。これは、移動またはスワップできるがコピーできないクラスの場合は特に間違っています。

一般的に、移動操作はスローすることが許可されているためです

彼らは間違いなくそうではありません。移動がスローされる可能性がある場合、移動を伴うほとんどすべての状況で、強力な例外安全性を保証することは事実上不可能です。メモリからの標準ライブラリのC++0x定義は、移動時に標準コンテナで使用可能なタイプをスローしてはならないことを明示的に示しています。

これはそれが得るのと同じくらい効率的です

それも間違っています。オブジェクトの移動は純粋にメンバー変数であると想定していますが、すべてではない場合があります。実装ベースのキャッシュを使用している可能性があり、クラス内でこのキャッシュを移動しないことを決定する可能性があります。実装の詳細として、移動する必要がないと私が考えるメンバー変数を移動しないことは完全に私の権利の範囲内です。ただし、それらすべてを移動する必要があります。

これで、サンプルコードが多くのクラスで有効になるはずです。ただし、完全かつ完全に正当な多くのクラスでは非常に確実に無効であり、さらに重要なことに、操作をその操作に減らすことができれば、とにかくその操作にコンパイルされます。これは完全に良いクラスを破っていますが、まったく利益はありません。

于 2011-02-02T16:22:39.660 に答える
1

swap誰かが多形型でそれを使用すると、あなたのバージョンは大混乱を引き起こします。

検討:

Base *b_ptr = new Base();    // Base and Derived contain definitions
Base *d_ptr = new Derived(); // of a virtual function called vfunc()
yourmemcpyswap( *b_ptr, *d_ptr );
b_ptr->vfunc(); //now calls Derived::vfunc, while it should call Base::vfunc
d_ptr->vfunc(); //now calls Base::vfunc while it should call Derived::vfunc
//...

これは間違っています。これは、bにDerivedタイプのvtableが含まれているため、タイプがないDerived::vfuncオブジェクトで呼び出されるためですDerived

通常std::swapはのデータメンバーのみを交換するBaseので、これは問題ありませんstd::swap

于 2011-02-02T14:50:08.240 に答える