unique_ptr で今風な C++ コードを書こう!!

はじめに

お久しぶりです。KMC OB の id:nojima です。

この記事は KMC Advent Calendar 2014 の10日目の記事です。 昨日は id:murata さんの「受験生応援!Javascriptでひねくれ数列」 でした。

今日は C++ の unique_ptr の話です。 (最初は rvalue について書こうと思っていたのですが、書いてみると unique_ptr だらけになったのでタイトルを変えました。なので、KMC Advent Calendar 2014 に書いてあるタイトルとは食い違っています。すみません)

個人的には C++03 ではなく C++11 を使う最大の理由は unique_ptr の存在だと思っています。

  • 例外発生時にももれなく delete してくれる。
  • 生ポインタとパフォーマンスが同じ。(最適化されている場合)
  • 所有権を型として表明できる。

など、様々なメリットがあり、ソースコードに unique_ptr という文字列が存在するだけで何となく今風な雰囲気が漂ってくる優れものです。

ということで、今回は unique_ptr にまつわる tips 集です。

生ポインタではなく unique_ptr を返す関数を書こう

ゲームを制作している状況を考えます。 ステージのレベルに合わせて Enemy を作成して返す関数 CreateEnemy は以下のように書くことができます。

unique_ptr<Enemy> CreateEnemy(int level) {
    if (level <= 5)
        return unique_ptr<Enemy>(new WeakEnemy());    // 低い階層では弱い敵を返す
    else
        return unique_ptr<Enemy>(new StrongEnemy());  // 高い階層では強い敵を返す
}

...

unique_ptr<Enemy> enemy = CreateEnemy(10);

CreateEnemy をこのように unique_ptr を使って書くメリットは2つあります。

  • Enemy* が登場しないので、メモリリークを心配する必要はありません。 unique_ptr を使っている限り、適切なタイミングでメモリが解放されることが保証されます。 もちろん、CreateEnemy の利用者側で unique_ptrget 関数を呼び出して生のポインタを得た場合はこの限りではありませんが、その場合でも use-after-free の可能性を get を利用している箇所に限定でき、デバッグが容易になります。
  • 戻り値の型から EnemyCreateEnemy の利用者側が delete すべきであることが簡単にわかります。 Enemy* を返す関数の場合、その関数の利用者が Enemy* を削除していいのかわかりません。 もしかすると、static な領域に Enemy を確保しており、それへのポインタを返している可能性もありますし、複数回の関数コールでメモリ領域を共有しているため、簡単には delete できないかもしれません。 unique_ptr を返している場合は、型から利用者に delete の責任があることがわかるので、こういった心配をする必要はありません。

また、unique_ptr を返す関数を shared_ptr で受けることもできます。このため、複数の場所で Enemy を共有したい場合でも CreateEnemy の戻り値の型を shared_ptr に変更する必要はありません。

shared_ptr<Enemy> enemy = CreateEnemy(10);

unique_ptr&& を受け取って別の関数に渡すときは move しよう

unique_ptr を受け取って、それをそのまま別の関数に渡したい場合があります。 特に、クラスのコンストラクタunique_ptr<T>&& を受け取って unique_ptr<T> 型のメンバ変数を初期化するようなことは頻繁に発生します。

ところが、普通に以下のように書くことはできません。

// 背景
class Background {
public:
     // 受け取った bitmap を利用してメンバ変数を初期化する。
     // unique_ptr はコピーできないので move したい。
     Background(unique_ptr<Bitmap>&& bitmap)
         : bitmap_(bitmap) {}

private:
     unique_ptr<Bitmap> bitmap_;
};

clang に書けると以下のようなエラーになります。

t.cpp:63:11: error: call to deleted constructor of 'unique_ptr<Bitmap>'
        : bitmap_(bitmap) {}
          ^       ~~~~~~
/usr/include/c++/4.8/bits/unique_ptr.h:273:7: note: 'unique_ptr' has been
      explicitly marked deleted here
      unique_ptr(const unique_ptr&) = delete;
      ^
1 error generated.

エラーメッセージにあるとおり、unique_ptr(unique_ptr&&) を呼びたかったのに、unique_ptr(const unique_ptr&) を呼んでしまっています。 しかし、bitmap の型は unique_ptr<Bitmap>&& のはずなのに、なぜ unique_ptr(unique_ptr&&) ではなくて unique_ptr(const unique_ptr&) が呼ばれてしまうのでしょうか?

その理由は、unique_ptr(unique_ptr&&) の方を呼ぶためには引数の型が rvalue reference であるだけでは不十分で、引数の式の value category が rvalue であることも要求されるからです。この場合は、引数の式の value category が lvalue なので、unique_ptr(const unique_ptr&) の方を呼びだそうとしてしまいます。 (lvalue は、bitmapbitmap.width などの「名前のついたオブジェクト」を表す式です。rvalue は lvalue の逆で foo()x + 1 といった「名前のないオブジェクト」を表す式です。)

これを解決するためには以下のように std::move 関数を使って引数の式を rvalue にすればよいです。

// 背景
class Background {
public:
     // 受け取った bitmap を利用してメンバ変数を初期化する。
     Background(unique_ptr<Bitmap>&& bitmap)
         : bitmap_(move(bitmap)) {}    // move すれば OK

private:
     unique_ptr<Bitmap> bitmap_;
};

vector<unique_ptr<...>> を使うときは make_move_iterator を活用しよう

unique_ptr の vector などを作っていると、各要素を move するような iterator が欲しいことがあります。 std::make_move_iterator 関数を使うと、まさにそのような iterator を作ってくれます。

またゲームの例ですが、2つの vector があって、片方の vector からもう片方の vector に要素を移動させたい場合を考えます。

// 全 Enemy を格納する vector
vector<unique_ptr<Enemy>> all_enemies;
// 現在のフレームで生成された Enemy を格納する vector
// (現在のフレームが終わるまでは all_enemies には登録されない)
vector<unique_ptr<Enemy>> new_enemies;

...

// new_enemies に入っている Enemy を全て all_enemies に移動させる。
all_enemies.insert(all_enemies.end(),
                   make_move_iterator(new_enemies.begin()),
                   make_move_iterator(new_enemies.end()));
new_enemies.clear();

こんな感じに、make_move_iteratorvector::insert で簡単に要素を移動させることができます。 また、<algorithm> の関数とも組み合わせることもできて便利なので、覚えておくといろんな所で役に立つと思います。

終わりに

ということで、unique_ptr を積極的に活用して delete をソースコードから駆逐しましょう!!

明日は id:nonylene さんの「電電宮にお参りした話」です。お楽しみに!!

追記 (12/10)

id:cos65535 さんの指摘により訂正

メモリリークの可能性を get を利用している箇所に限定でき」
  ↓
「use-after-free の可能性を get を利用している箇所に限定でき」