2012-03-22

C++11のstd::swapはC++03のstd::swapとは互換性がない

C++11のstd::swapは、だいぶ大きく変更された。C++03までのswapは、<algorithm>にあり、CopyConstructibleかつAssignable(C++11のCopyAssignableに相当)を要求した。ところが、C++11では、<utility>に移されたあげく、MoveConstructibleかつMoveAssignableを要求するようになった。まるっきり別物になってしまっているのだ。果たして問題を起こさないものだろうか。おぼつかないものだ。

実装例は以下のようになる。

// C++03のswap
template < typename T >
void swap( T & a, T & b )
{
    T temp = a ;
    a = b ;
    b = temp ;
}

// C++11のswap
template < typename T >
void swap( T & a, T & b )
{
    T temp = std::move(a) ;
    a = std::move(b) ;
    b = std::move(temp) ;
}

ところで、Andrew Koenigは、swapはコピーやムーブ以上に基本的な操作だと書いている。

Object Swapping Part 1: Its Surprising Importance | Dr Dobb's
Object Swapping, Part 2: Algorithms Can Often Swap Instead of Copy | Dr Dobb's

C++11のswapはムーブになっているので、わざわざメンバー関数としてのswapを実装する必要はない。ムーブコンストラクターさえ実装しておけば、それがそのままstd::swapで使える。さて、koenigの理論を実践してみる。

class X
{
private :
    std::size_t size ;
    char * ptr ;

public :
    X( std::size_t size )
        : size(size), ptr( new char[size] )
    { }
    ~X()
    {
        delete[] ptr ;
    }

    X( X const & t )
        : size( t.size ), ptr( new char[t.size] )
    {
        std::copy( t.ptr, t.ptr + size, this->ptr ) ;
    }

    X ( X && t ) 
        : size( t.size ), ptr( t.ptr )
    {
        t.size = 0 ;
        t.ptr = nullptr ;
    }

    X & operator = ( X const & t )
    {
        if ( this == &t )
            return *this ;

        X temp = t ;
        std::swap( *this, temp ) ;
        return *this ;
    }

    X & operator = ( X && t )
    {
        if ( this == &t )
            return *this ; 

        std::swap( *this, t ) ;
        return *this ;
    }
} ;

なるほど、コピー/ムーブコンストラクター以外はすべてswapで実現可能なのだな。

4 comments:

Jesse said...

パフォーマンス的に以下はどうでしょうか?
通常は、std::swapでmoveされているため、delete [] ptr;がno-opになるので、std::swapを使うより早いと思います。

X& operator=(X&& t)
{
assert(*this != &t);
delete [] ptr; // no-op
size = t.size;
ptr = t.ptr;
t.size = 0;
t.ptr = nullptr;
return *this;
}

江添亮 said...

ムーブ後のオブジェクトがすぐに破棄されるならば、結果的には全く同じ事です。
代入演算子でswapを使うと、ムーブ後のオブジェクトは、ムーブ先で確保されたストレージを所有することになるので、
その後普通に使うこともできます。

swapを使うと、ストレージの破棄を必要になるまで遅らせることができるのです。

Jesse said...

X x1(10);
X x2(10);
x1 = x2; // #1

ムーブ代入演算子でstd::swap( *this, t ) ;を呼び出すと、swap内部でムーブ代入演算子がまた呼び出されるので、、#1で無限ループに落ちるのでは?

江添亮 said...

そういえばそうだ。
やはり自前のスワップは必要かな。