C++0xの新機能である右辺値参照を試したいが為にVisual Studio 2010 Bata1を導入。
Microsoftのページから無料でダウンロードできる。
Visual Studio Team System 2010 Team Suite Beta 1 というタイトルだがこれでOK。
その前によく言われているC++0xらしいプログラムを。
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
void main(void){
std::vector<std::shared_ptr<std::string>> strs(2);
strs.at(0).reset(new std::string("World!!"));
strs.at(1).reset(new std::string("Hello "));
auto begin=strs.begin();
auto end=strs.end();
std::reverse(begin,end);
auto func=[](std::shared_ptr<std::string>& p)->void{
std::cout << *p << std::endl;
};
std::for_each(begin,end,func);
}
まず1つめはBoostライブラリの一部が標準ライブラリに編入されました。
shared_ptr・weak_ptrなどがmemory、ref・cref・bindなどがfunctionalに入って、std名前空間になったと。
というわけでBoostを用意しなくてもshared_ptrが使えます。
残念ながらscoped_ptrは対象ではないのだが、まぁauto_ptrを注意して使えばOKだろう。
2つめは型推論のautoが使えるようになった。長すぎる型名はautoを使えば便利ですね。
std::vector<std::shared_ptr<std::string>>::iteratorとかいう長すぎる型名はautoでOKですね。
3つめはラムダ式が導入されたこと。[](引数)->返り値型{ }の形式で書く。返り値型は類推できるとき省略できる。
[]って何なのかというとローカル変数の取り扱いを決めるときに使う。
[&]だと参照渡し、[=]だと値渡しで変更不可、[&](void)->void{x++;}のように使う。
そういえばVS2010ではデバッグなしで実行しても画面が閉じてしまうねぇ。
{ char dummy; std::cin >> dummy; }
とか書いておきましょう
さて、それで問題なのは右辺値参照である。これ言葉だけ聞いてもわけがわからない。
まずC++でいう右辺値というのを理解しないといけない。
右辺値というのは右辺にしか来ることができない式の種類のこと。
具体的にはリテラルと参照戻し(返り値がT&)以外の関数の返り値のこと。
これらは一時的なオブジェクトであるから右辺にしか来ることがないよねってこと。
std::vector a=CreateString();
ここでCreateString();の返り値のオブジェクトはコピーコンストラクタに渡される。
そしてそれが終わったら破棄される。実にもったいない。
ここで文字列をはぎ取ってしまえればなぁと。どうせ捨てられるものならいいじゃない。
これが右辺値参照という技でできるようになると。
こういう使い方に注目した例というのが無くて困った。
というわけで作ってみた。
class MyObj{
public:
char Key;
MyObj(char key):Key(key){
std::cout << "Create MyObj : " << this->Key << std::endl;
}
MyObj(const MyObj& other):Key(other.Key){
std::cout << "Copy MyObj : " << this->Key << std::endl;
}
~MyObj(void){
std::cout << "Destroy MyObj : " << this->Key << std::endl;
}
};
class Test{
private:
std::auto_ptr<MyObj> p;
public:
Test(char key){
std::cout << "Create Test : " << key << std::endl;
this->p.reset(new MyObj(key));
}
Test(const Test& other){
std::cout << "Copy Test : " << other.p->Key << std::endl;
this->p.reset(new MyObj(*other.p));
}
Test(Test&& other){
std::cout << "Move Test : " << other.p->Key << std::endl;
this->p=other.p; //所有権移動
}
~Test(void){
if(this->p.get()==0){
std::cout << "Destroy Test : [Nothing]" << std::endl;
}else{
std::cout << "Destroy Test : " << this->p->Key << std::endl;
}
}
};
Test Create(void){
Test result('R');
return result;
}
void main(void){
{
Test l1('L');
Test l2(l1); //LValue用コピーコンストラクタ
}
{
Test r1(Create()); //RValue用コピーコンストラクタ
Test r2(std::move(r1)); //r1は破壊される
}
}
とはいえ長いのよねぇ。これはMyObjのポインタを持つTestというクラスを使ってやっている。
右辺値参照というのはT&&型を引数に取ることなのだが、これはconst T&を引数に取る関数とセットで使う。
コピーコンストラクタとかってconst T&を引数に取るよね。
けど引数に取るオブジェクトを破壊してもよろしいというのならばもっと高速にできるんです、
というときはT&&を引数に取るやつを作ってくれということです。
というわけでTestのコピーコンストラクタはconst T&版とT&&版の2つがある。
const T&版はポインタのさすMyObjをMyObjのコピーコンストラクタでコピーしている。
それに対してT&&版はポインタで奪い取っている。
なのでMyObjを作り直さなくて済むと。
実行するとMyObjの作成・破棄、Testの作成・破棄が見えるので実行してみる。
Create Test : L //l1の作成
Create MyObj : L
Copy Test : L //l2の作成(LValue用コピーコンストラクタ)
Copy MyObj : L
Destroy Test : L //l2を破棄
Destroy MyObj : L
Destroy Test : L //l1を破棄
Destroy MyObj : L
Create Test : R //Create()内部での作成
Create MyObj : R
Move Test : R //r1の作成(RValue用コピーコンストラクタ)
Destroy Test : [Nothing] //Create()で作成したオブジェクト破棄
Move Test : R //r2の作成(RValue用コピーコンストラクタ)
Destroy Test : R //r2を破棄
Destroy MyObj : R
Destroy Test : [Nothing] //r1を破棄
//によるコメントは解説のために書き足した。
まず初めは左辺値l1をコピーする実験。l2はl1をまともにMyObjもコピーしていることがわかる。
左辺値は標準では破壊されないことになっている。というのがコピーコンストラクタの基本です。
例外はありますよ。std::auto_ptrとか。けどトラブルの原因だから普通はしない。
引数の型名、const T&は参照渡しでもらうけど中身は変えませんという意味だから。
まともにコピーしているのでl1はそのままの状態で残っている。
なのでl1もl2もまともに破棄しなければならない。
一方、右辺値Create()をコピーする実験。r1はその返り値を破壊してコピーする。
なのでMyObjのコピーは起きていません。ポインタを奪い取るだけなので。
その後Create()の返り値は破棄されますが、[Nothing]、すなわちポインタを奪い取られたことを示しています。
今まではこういうコピーが多かったってことです。std::stringが返り値ならばその中身をいちいちコピーしてたと。
それがstd::string::swap()程度の動作で済むんじゃないかということだと思います。
さて、さっきはl2からl1のコピーはまともにしましたが、もしl1を破壊してもかまわないので高速にやってほしい。
そういう要望はあるかもしれません。もういらんから好きにしてくれと。
そういうときはstd::moveにかぶせて渡すといいわけです。
実際r1からr2へのコピーでそれを行っていますが、これもCreate()の返り値からr1へのコピー同様にやっています。
そして破棄されるときr1は[Nothing]となっているからポインタは奪い取られていると。
こういうこともできるわけです。
ちなみに右辺値参照のコピーコンストラクタを消すとどうなるか。
さっきのr1の作成より先からだけ示します。
Copy Test : R //r1の作成(全部コピー)
Copy MyObj : R
Destroy Test : R //Create()で作成したオブジェクトを全部破棄
Destroy MyObj : R
Copy Test : R //r2の作成(全部コピー)
Copy MyObj : R
Destroy Test : R //r2を全部破棄
Destroy MyObj : R
Destroy Test : R //r1を全部破棄
Destroy MyObj : R
もしMyObjがstd::stringでこれが何個もあったら、ぞっとする差ですね。
しかしこういう悩みはC++らしい悩みではありますよね。
C++は実体を返すのが一般的なわけですが、JavaやC#の場合は返り値も引数も全部参照。
多分JavaやC#で上みたいな自体に遭遇した人は多分いないんじゃないかなと思う。
僕はよくstd::stringを返り値にするときboost::shared_ptr(C++0xではない!!)で包んだポインタを渡すわけですが、
これはstd::stringをコピーすると大変だからコピーしなくて済むようにポインタで返そうと。
JavaやC#の考え方にそっくりですね。けど多分C++的ではないんでしょう。
しかしなんとC++はコピーを簡略化するための方法を用意してきたという驚きの展開となるわけです。
C++とは恐ろしいものだ。化け物じみている。
ちなみにこの機能は自分で理解しなくてもうまみは十分あります。
なぜならばライブラリに右辺値用のものが用意されていれば、右辺値ならば勝手にそれを使ってくれるから。
ええ、C++の恐ろしいところってそこなんですよ。勝手にやってしまうあたり。
どこかに「ただでさえ速いSTLがさらに速くなるのか」とか書いてあったけど、そういう認識でもOKだと思います。
言語の拡張の割には冷ややかな反応ですけど別にそれでもいいと思います。