いさぽん部屋(isapon.com)

ゲーム系プログラマによる特に方針のないブログ。技術系とカレー、ラーメンネタ多めだったはずが、最近はダイエットネタ多め。

c++11 std::move /ポインタのムーズでハマりそうなこと

C++11ネタです。俺の中ではC++といってもC++11以前と以降では別物として考えています。


Before C++11 と After C++11 、略してBC11とAC11。なんだか西暦っぽいです。でもそれくらいC++はC++11で変わりました。


と、そんなC++11ですが、C++11以降で結構肝になるのがムーブセマンティック。細かいことは一旦置いておくと、ムーブコンストラクタの扱いが肝になります。


んで、ムーブコンストラクタ(アサインなんかもあるけどとりあえず本題と関係ないのでそのあたりはおいといて)で疑問に思ったことがあったのでメモ書き。


その1)shared_ptrをムーブしてみよう

C++ではポインタは極力生で扱わないのがコツだと思います。


というわけで、多くの場合 std::shared_ptr を使うわけですが、C++11 のムーブを使うと以下のようになります。


std::shared_ptr<int>    p1  = std::make_shared<int>(1);
std::shared_ptr<int>    p2;

p2  = std::move(p1); // ここがムーブ

std::cout << "p1=" << p1.get() << ", p2=" << p2.get() << std::endl;

何をやっているかというと、p1に入れたポインタをムーブを使ってp2に移動し、p1とp2のポインタの値を表示しているだけです。


結果は

p1=0, p2=0x212e028

となり、p2へ移動したp1は、nullptr になります。想像通りの挙動ですね。


その2)生ポインタならどうだ!?

略して生ポ。なんだか生焼けな感じのする響きです。激しくどうでもよいうえに何も面白みもありません。なまぽ。


生のポインタなんて反復処理以外ではあんま使わないので、あまり気に留めていなかったのですが、テンプレートを駆使していた時にふと「あれ?これどういう仕様だっけ?」と気になったので調べてみました。


int     num = 1;
int*    p1  = てきとーな値
int*    p2;

p2  = std::move(p1);
std::cout << "p1=" << p1 << ", p2=" << p2 << std::endl;

先ほどの std::shared_ptr と基本的には同じです。こうするとどうなるかというと

p1=0x7fffef84b210, p2=0x7fffef84b210

という感じで、移動というよりはコピーになってしまいました。つまり、以下のようなコードを書くとよくない結果になります。


struct int_object
{
    int_object()
    {
        nama_pointer = new int;
    }

    int_object(my_pointer&& _s)
          : nama_pointer(std::move(_s.nama_pointer)) {}

    ~int_object()
    {
        if (nama_pointer) delete nama_pointer;
    }

    int* nama_pointer;
};

これでムーブをすると、一つのポインタに対してdeleteが2回呼ばれてしまうので当然予期せぬ挙動になります。


移動の定義について考える

たぶんどこかに詳しい仕様があるはずなのですが、見つけきれず(見落としたかも)で申し訳ないのですが。


移動はあくまでも「移動先へ値をコピーし、移動元は初期値にする」と覚えるとわかりやすいです。


それを疑似コードにするとこんな感じになります。


move(type& source, type& destinate)
{
    destinate = source;
    new (&source) type();
}

平たく言うと、値をコピーした後にデフォルトコンストラクタで初期化するということですね。


つまり、生のポインタのデフォルトコンストラクタは何もしません。nullptr で初期化もしません。なので、移動元のポインタは残ったままということになります。


そう思うと、簡単ですね。