Boost.Move 解説 (前編)

はじめに

Boost.Move は、C++0x で導入される move sematics を C++03 でエミュレートするべく、Ion Gaztanaga 氏 (Boost.Interprocess の作者) によって開発されたライブラリです。既に Boost 入りが決定しており、現在 trunk にあります。
この記事は、その使い方と実装を日本語で解説するために書かれました。C++0x の move semantics を大まかに理解している人を対象にしています。
参照
本の虫: rvalue reference 完全解説
【5日目】Boost.Moveが気になって - Flast?なにそれ、おいしいの?

ムーブ可能なクラスを定義する

クラスを move semantics に対応させるためには、Boost.Move の作法に則って定義しなければなりません。コピー可能かつムーブ可能なクラスを定義する例を示しましょう。

#include <boost/move/move.hpp>

// int * を内包するクラス
class integer {
    BOOST_COPYABLE_AND_MOVABLE(integer)         // 1.

    int *value_;

public:
    integer() : value_(new int()) {}
    integer(int value) : value_(new int(value)) {}

    integer(integer const &x)                   // 2.
      : value_(new int(*x.value_)) {}

    integer(BOOST_RV_REF(integer) x)            // 3.
      : value_(x.value_) { x.value_ = 0; }

    ~integer() { delete value_; }

    integer &operator=(BOOST_COPY_ASSIGN_REF(integer) x) // 4.
    {
        if (this != &x) {
            delete value_;
            value_ = new int(*x.value_);
        }
        return *this;
    }

    integer &operator=(BOOST_RV_REF(integer) x) // 5.
    {
        if (this != &x) {
            delete value_;
            value_ = x.value_;
            x.value_ = 0;
        }
        return *this;
    }
};
  1. BOOST_COPYABLE_AND_MOVABLE(type) を private セクションに記述します。
  2. コピーコンストラクタは通常通り定義します。暗黙に定義されるものを使うこともできます。*1
  3. ムーブコンストラクタは、引数を BOOST_RV_REF(type) として宣言します。この型は、ほとんど type & と同様に扱うことができます。
  4. コピー代入演算子は、引数を type const & ではなく、BOOST_COPY_ASSIGN_REF(type) として宣言します。この型は、ほとんど type const & と同様に扱うことができます。コピー代入演算子は必ず定義しなければなりません。
  5. ムーブ代入演算子は、引数を BOOST_RV_REF(type) として宣言します。

コピー不可かつムーブ可能なクラスを定義する場合は、マクロ BOOST_COPYABLE_AND_MOVABLE の代わりに BOOST_MOVABLE_BUT_NOT_COPYABLE を使用し、コピーコンストラクタとコピー代入演算子は宣言しません。
ムーブ可能なクラスを定義する方法はこれだけです。こうして定義したクラスは、ほとんど C++0x の move semantics に従って動作します*2。明示的に move する場合は、std::move の代わりに boost::move を使います。
では実装について考察しましょう。

Move semantics のエミュレーション

Lvalue と rvalue を識別する

コピーコンストラクタとムーブコンストラクタ、コピー代入演算子とムーブ代入演算子を呼び分けるには、lvalue と rvalue を識別することが必要不可欠です。どうすればいいでしょうか。
任意の型*3 T については、T の rvalue は T const & でしか受けることができません。しかし、T の lvalue もまた T const & で受けることができますから、一度 T const & で受けてしまうと、引数が lvalue だったのか rvalue だったのかはわからなくなってしまいます。
では、T & を受けるオーバーロードと T const & を受けるオーバーロードを用意するのはどうでしょうか。T の lvalue を渡すと前者が呼び出され、T の rvalue を渡すと後者が呼び出されるので、これで T の lvalue と T の rvalue を区別することができます。しかし T const の lvalue は後者を呼び出してしまうので、依然として T の rvalue との区別ができません。
任意の型 T についてはこれが限界です。しかし、クラス型にある型変換演算子を侵入的に定義することができれば、lvalue と rvalue を識別することが可能になります。そこで登場するのが rv です。

rv によるオーバーロード

rv は 次のように定義されます。

template <class T>
class rv : public T { /* ... */ };

T から派生しただけの単純な型です。ではクラス型 hoge に、rv & / rv const & への型変換演算子を定義してみましょう。

class hoge {
public:
    operator rv<hoge> &() { return *static_cast<rv<hoge> *>(this); }
    operator rv<hoge> const &() const { return *static_cast<rv<hoge> const *>(this); }
};

static_cast*4 を使って、自らを rv への参照にダウンキャストしています。
これによって何が可能になるのでしょうか。次の3つの関数を見てください。

void f(hoge &);           (A)
void f(rv<hoge> const &); (B)
void f(rv<hoge> &);       (C)

まず hoge の lvalue は、通常通り (A) を呼び出します。次に hoge const の lvalue または rvalue は、const 版の型変換演算子によって rv const & に変換され、(B) を呼び出します。そして hoge の rvalue は、非 const 版の型変換演算子によって rv & に変換され、(C) を呼び出します。
結果、見事 hoge の rvalue だけを (C) に振り分けることに成功しました。hoge const の lvalue と rvalue は両方 (B) を呼び出すので区別できませんが、実質これらを区別する意義はないので、問題ないでしょう。
では最初の integer の定義に戻って、Boost.Move が何をしているのかを明らかにしましょう。

代入演算子の実装

まず最初のマクロ呼び出しに注目しましょう。

    BOOST_COPYABLE_AND_MOVABLE(integer)

このマクロは二つの役割をしています。一つは前述の型変換演算子を実装すること、もう一つは代入演算子の上記 (A) 版 (integer & を引数に取るもの) を実装することです。展開してみましょう。

public:
   integer& operator=(integer &t) // 代入演算子 (A)
   {  this->operator=(static_cast<const ::boost::rv<integer> &>(const_cast<const integer &>(t))); return *this;}
public:
   operator ::boost::rv<integer>&() 
   {  return *static_cast< ::boost::rv<integer>* >(this);  }
   operator const ::boost::rv<integer>&() const 
   {  return *static_cast<const ::boost::rv<integer>* >(this);  }
private:

代入演算子については (A) と (B) を区別する意義は通常ありませんから、(A) はそのまま (B) を呼び出すように実装されています。ではその (B) はどこにあるのでしょうか。それは、我々が自らコピー代入演算子として実装しました。

    integer &operator=(BOOST_COPY_ASSIGN_REF(integer) x); // コピー代入演算子 (代入演算子 (B))

コピー代入演算子*5の引数を integer const & ではなく BOOST_COPY_ASSIGN_REF(integer) として宣言した理由が、ここで明らかになります。BOOST_COPY_ASSIGN_REF(integer) は boost::rv const & に展開され、integer const の lvalue と rvalue を受け入れているのです。そして boost::rv は integer から派生していますから、引数は integer const & として扱うことができます。
最後にムーブ代入演算子を見てみましょう。

    integer &operator=(BOOST_RV_REF(integer) x); // ムーブ代入演算子 (代入演算子 (C))

もう説明の必要はないでしょう。BOOST_RV_REF(integer) は boost::rv & に展開され、integer の rvalue のみを受け入れています。

コンストラクタの実装

コピーコンストラクタとムーブコンストラクタの呼び分けは、少々事情が違います。我々は integer のコピーコンストラクタを、integer const & を引数に取るように定義しました。

    integer(integer const &x);

T const & は、lvalue と rvalue の区別をつかなくしてしまう凶器でした。実際その通りなのです。次のコードを考えます。
test.cpp

#include <iostream>
#include <boost/move/move.hpp>

class test
{
    BOOST_COPYABLE_AND_MOVABLE(test)

public:
    test() {}
    
    test(test const &) { std::cout << "copy constructor\n"; }

    test(BOOST_RV_REF(test)) { std::cout << "move constructor\n"; }

    test &operator=(BOOST_COPY_ASSIGN_REF(test)) { return *this; }
};

test create_test() { return test(); }

int main() { test t(create_test()); }

コンパイルして実行します。

$ g++ -fno-elide-constructors test.cpp
$ ./a.exe
copy constructor
copy constructor

ムーブコンストラクタが呼び出されるべきところ、コピーコンストラクタが呼び出されてしまっています。
しかし心配は無用です。今回は -fno-elide-constructors オプションで意図的に抑制しましたが、この場合は RVO という最適化によって、コピーコンストラクタの呼び出しを省略することができるのです。ムーブより RVO の方がさらに高速なため、Boost.Move はそれを殺さない戦略をとっているということです。
ムーブコンストラクタを呼び出さなくてはならない場合は、明示的に boost::move を呼び出すことによって、T を boost::rv & に変換することができます。

基底クラスまたはメンバオブジェクトを持つクラスのムーブ

実装の解説は一旦終わりにして、基底クラスまたはメンバオブジェクトを持つクラスをムーブ対応させる方法を示しましょう。

class derived : public base
{
    BOOST_COPYABLE_AND_MOVABLE(derived)
    
    member mem;

public:
    derived() : base(), mem() {}

    // コピーコンストラクタ
    derived(derived const &x)
      : base(x),
        mem(x.mem)
    {}

    // ムーブコンストラクタ
    derived(BOOST_RV_REF(derived) x)
      : base(boost::move(static_cast<base &>(x))), // 1.
        mem(boost::move(x.mem))
    {}

    // コピー代入演算子
    derived &operator=(BOOST_COPY_ASSIGN_REF(derived) x)
    {
        base::operator=(x);
        mem = x.mem;
        return *this;
    }

    // ムーブ代入演算子
    derived &operator=(BOOST_RV_REF(derived) x)
    {
        base::operator=(boost::move(static_cast<base &>(x))); // 2.
        mem = boost::move(x.mem);
        return *this;
    }
};

基本的には C++0x での定義と変わりありません。しかし重要な違いがあります。コメント 1. 2. の箇所に注目してください。BOOST_RV_REF(derived) x として受け取った引数を、base & にキャストしてから boost::move に渡しています。
なぜ boost::move(x) では駄目なのでしょうか。それは、boost::move(x) によって得られる rv & *6は、rv<base> & とは互換性がない型だからです (rv の定義を思い出してください)。コンストラクタについて言えば、rv & を base に渡した場合、base にムーブコンストラクタ base(rv<base> &) が用意されていたとしても、それは呼び出されません。さらに悪いことに、base にコピーコンストラクタ base(base const &) があった場合は、警告もなくそちらが呼び出されてしまうのです (!)。結果、x を base & にキャストするという過程が必要になります。
派生クラスでムーブコンストラクタ・ムーブ代入演算子を定義する場合は、この点に注意しなければなりません。

後編に続く

*1:2011/05/09 追記: C++0x では、ムーブコンストラクタを宣言した場合、暗黙のコピーコンストラクタは deleted として定義されます

*2:NRVOが働かないなど、重要な違いもあります

*3:cv-qualified でない object type

*4:以前は reinterpret_cast が使われていました

*5:実は、先ほどの (A) こそが本当のコピー代入演算子ですが、ここではこの (B) をコピー代入演算子と呼称します

*6:boost::move は、rv を受け取った場合そのまま返します