Hatena::ブログ(Diary)

joynote break;

2011-12-14

[][] move semanticsについて

人/後輩に教える時用の脳内カンペ。

ムーブセマンティクス(move semantics)というのは、コピーではなく、所有権の移動をさせるように動作するような考え方のこと。

例えば、下記のコードで

MyClass a; // aというインスタンスを作成
MyClass b = a; // aの中身の所有権がbに移動(move)

というような動作をするように書く感じ。

C++11でmove semanticsがッ! という言葉面だけ見ると、C++11があたかも全く新しい概念に対応したかのように(少なくとも昔の自分には)見えてしまう。

が、moveという考え方は昔から存在し、C++03でも書くことは可能だった。

例えば、こんな感じ。

struct MyClass{
    int* value;
    MyClass():value( NULL ){}
    MyClass(int num):value( new int(num) ){}
    ~MyClass(){ delete value; }
    void Move(MyClass& temp){
        value = temp.value;
        temp.value = NULL;
    }
};
int main(){
    MyClass a(100); // aというインスタンスを作成
    MyClass b;      // bという中身が空(value==NULL)のインスタンスを作成
    b.Move(a); // aの中身の所有権がbに移動(move)
}

ああ、まぁそう書けばそうなるけどさ……というレベルの簡単なコードです、が。

この書き方は、意識的に書く必要があって、面倒くさい。

operator =とか、コピーコンストラクタでこういう動作をした方が明らかにスマート。

でも、コピーのつもりが移動(move)されてたなんて事になったら、凄まじいバグの温床になる(有る筈の物が移動してどっか行っちゃうわけなので)。

でも、不必要なコピーをムーブに置き換えられたら、早くなって素敵だよね……という訳で、速度狂のC++erは、

「じゃあ、"今後、明らかに不要"なのを判別できるようにして、それだけを移動の対象にすればよくね? そう、右辺値とか」

と考えた結果、C++11で導入された右辺値参照を引数に取るコンストラクタ/代入演算子で「移動(move)」を別途定義できるようにしたのでありました!

※右辺値とは、凄まじく簡単に語弊を恐れず言えば、通常のコード上では一時的に生成されたコピー"元"の値であり、コピー先の値にコピーが終わった段階で破棄されるもの。関数の返り値として帰ってきた値とか。


という事で、具体的なコードは↓らへん

http://d.hatena.ne.jp/joynote/20110822/1314012953

http://d.hatena.ne.jp/joynote/20110608/1307523140

C++11に対応しているコンパイラでは、標準ライブラリでもあるSTLmoveに対応しているので、std::vector<T>の中身に関しても上の方のコードのint* valueに対する処理のような感じになっており、vector内部のポインタの付け替えによって「移動(move)」が行われるようになっている。

つまり、

今までのコードを書き換えなくても早いよ!

前は意識してテクニカルに書かなきゃいけない部分が見た目簡単に書けるよ!

ということでした!

========================

厳密さを欠いている可能性は非常に高いので、ツッコミは待ってます。

2011-08-22

[][] 暗黙のmoveとNRVO

VS2010(VC10)にて関数で返す値についての扱いをついったーで突っ込まれて実際書いて確かめた時のメモ。

#include <iostream>
#include <vector>
#include <boost/timer.hpp>

struct Test{
	std::vector<int> tmp;
	Test(){
		std::cout << "コンストラクタ" << std::endl;
	}
	~Test(){
		std::cout << "デストラクタ" << std::endl;
	}

	Test(const Test& obj) : tmp( obj.tmp ){
		std::cout << "コピーコンストラクタ" << std::endl;
	}
	
	Test(Test&& obj){
		std::swap(tmp,obj.tmp);
		std::cout << "ムーブコンストラクタ" << std::endl;
	}
};


Test get(int n, boost::timer& timer){
	Test tmp;
	for(int i=0; i < n; ++i){
		tmp.tmp.push_back(i*2+3*i);
	}
	std::cout << timer.elapsed() << std::endl;
	timer.restart();
	return tmp;
}


int main(){
	boost::timer t;
	auto tmp = get(10000000,t);
	std::cout << t.elapsed() << std::endl;
}

実行結果:

コンストラクタ
0.072
0
デストラクタ

NRVOが聞いてコピーもムーブも起こらずにそのまま置き換えられた。

これが最速だが、ifでreturn分けたりすると最適化が切れる場合が多い。

そうするとどうなるか。てっきり普通にコピーされるのかなーとか思っていた、VC2008脳だった。が。


#include <iostream>
#include <vector>
#include <boost/timer.hpp>

struct Test{
	std::vector<int> tmp;
	Test(){
		std::cout << "コンストラクタ" << std::endl;
	}
	~Test(){
		std::cout << "デストラクタ" << std::endl;
	}

	Test(const Test& obj) : tmp( obj.tmp ){
		std::cout << "コピーコンストラクタ" << std::endl;
	}
	
	Test(Test&& obj){
		std::swap(tmp,obj.tmp);
		std::cout << "ムーブコンストラクタ" << std::endl;
	}
};


Test get(int n, boost::timer& timer){
	Test tmp;
	for(int i=0; i < n; ++i){
		tmp.tmp.push_back(i*2+3*i);
	}
	std::cout << timer.elapsed() << std::endl;
	timer.restart();
	if( n%2 == 0 ){
		return tmp;
	}
	tmp.tmp.push_back(1192);
	return tmp;
}


int main(){
	boost::timer t;
	auto tmp = get(10000000,t);
	std::cout << t.elapsed() << std::endl;
}

実行結果:

コンストラクタ
0.072
ムーブコンストラクタ
デストラクタ
0.001
デストラクタ

あ……ハイ。そうですよね、C++0x(11?)では関数内で宣言された変数が返り値になった場合には原則moveされるんでしたねそういえばはい。


という事で暗黙moveされんじゃねーかstd::moveとか書く必要あるのすげー特定場面じゃないですかやだー!!

ということでしたというメモ。

------------------------------

コメント頂いて気づいたけども、「暗黙変換(継承先スマポ=>継承元スマポのような)」の場合は暗黙moveもされないし、もちろんNRVOもかからないので、std::moveを使うのはすげー特定場面でなく、単に特定場面くらいのニュアンスになりそう。

もちろん暗黙変換考える際にはポリモーフィックな感じに使う場面がメインと考えられて、moveされなくても誤差い場合が多いだろうけれども、そうでない場面も全然ありうるので忘れてはいけない場合でした。

2011-06-28

[][] range-based for

gcc 4.6.1をビルドしたので動作確認。

#include <iostream>
#include <list>
#include <memory>

struct Task{
        int id;
        Task(int id) : id(id){
                std::cout << "Create Task : " << id << std::endl;
        }
        ~Task(){
                std::cout << "Delete Task : " << id << std::endl;
        }
        void update(){
                std::cout << "move Task : " << id << std::endl;
        }
};

int main(){
        std::list<std::unique_ptr<Task>> task_list;

        task_list.emplace_back(new Task(1));
        task_list.emplace_back(new Task(2));
        task_list.emplace_back(new Task(3));
        task_list.emplace_back(new Task(4));
        task_list.emplace_back(new Task(5));
        task_list.emplace_back(new Task(6));

        for(auto it = task_list.begin(); it != task_list.end();){
                if( (*it)->id%2 == 0 ){
                        it = task_list.erase(it);
                        continue;
                }
                it++;
        }
        // range-based for。
        // イテレータではなく中身がそのままitに代入して使われる(ぽい)
        // auto& でほぼ全て大丈夫なんじゃないか疑惑。これはいいものだ。
        for(auto& it : task_list ){
                it->update();
        }
}


実行結果:

Create Task : 1
Create Task : 2
Create Task : 3
Create Task : 4
Create Task : 5
Create Task : 6
Delete Task : 2
Delete Task : 4
Delete Task : 6
move Task : 1
move Task : 3
move Task : 5
Delete Task : 1
Delete Task : 3
Delete Task : 5

やっと……やっとC++にforeachがッ……!

2011-06-08

[][][] ムーブセマンティクス(Move Semantics)はテクニックにすぎない

正確には、C++0xではムーブセマンティクスを実現"しやすく"なっただけで、

ムーブ自体は昔からのテクニックに過ぎない。

という事をやっと理解したっぽい?メモ。

#include <iostream>
#include <vector>
#include <string>

std::vector<std::string> add_bar(std::vector<std::string> lhs)
{
  lhs.push_back("bar");
  return lhs;
}

int main(){
  const size_t v_size = 100000;
  std::vector<std::string> v1;
  std::vector<std::string> v2;

  v1.reserve(v_size*2);
  for(size_t i=0; i < v_size; ++i){
    v1.push_back("foo");
  }

  // クソみたいに重いコピーが起こる
  v1 = add_bar(v1);

  // moveされてすぐ終わる
  v1 = add_bar( std::move(v1) );

  // この時点でv1には100000個の"foo"と2個の"bar"が入っている。

  // moveする。つまり、"ムーブコンストラクタ(or代入演算子)を呼び出す"。
  v2 = std::move(v1);

  // この時点でv1には何も入っていない。
  // この時点でv2には100000個の"foo"と2個の"bar"が入っている。

  // そしてその処理は、std::vectorの中身を書いた人がポインタのつなぎ替え等で"実装したもの"である。

  return 0;
}

============================

Move Semanticsが???となる人の突っかかりポイントは(自分がまさにそうだったが)、新しく特別なメモリ上の操作が自動、あるいは新しい構文で行われるんじゃないと思っている所。

実際は単に、移動してよい場合というのを区別できるムーブコンストラクタやムーブ代入演算子が定義されただけで、中の実装は単なるポインタの操作である。(だから配列がポインタ等を用いず生で使ってある場合にはコピー==ムーブ)

実際にはSTLの内部処理とかに使われていて、一般のプログラマが気にする事ではないけれども、どういう事が行われているかを知っているのと知らないのではだいぶ違うよね的な。