スマートポインタとムーブセマンティクス
ここまでコピー、ムーブと扱ってきましたが、第3回で紹介したスマートポインタは、このムーブを実装したクラスです。ここからは、スマートポインタをムーブセマンティクスという観点でおさらいしてみます。
所有権のコピーを認めないスマートポインタ[C++14]
最初は、所有権のコピーを認めないスマートポインタunique_ptrです。所有権、覚えていますか?値の保持や変更を許される代わりに、メモリ領域の管理責任を負うという仕組みでした。このことから、所有権とは値そのもの、メモリ領域そのものとも言えると筆者は思っています。
スマートポインタunique_ptrは、初期値込みでnew演算子も使わずに安全に初期化できるstd::make_unique<T>関数を使って宣言するのでした。make_unique<T>関数は、文字通り所有権を保有するインスタンスを1個しか認めないスマートポインタを生成します。unique_ptr自体はC++ 11から利用可能ですが、make_unique<T>関数はC++ 14からのサポートです。
auto u_str_ptr = make_unique<string>("Hello, World!!");
これで、所有権と一対一となるunique_ptrというスマートポインタが出来上がります。型はstringで、値は"Hello, World!!"です。このとき、u_str_ptrは"Hello, World!!"が置かれたメモリ領域を「所有」します。所有しているものは、いつかは手放すときが来ます。いつ来るかというと、通常それはスコープを外れたときです。一般的には、ブロックや関数の出口となります。所有権の喪失とともに、所有していたメモリ領域が自動で解放されます。
{ auto u_str_ptr = make_unique<string>("Hello, World!!"); cout << *u_str_ptr << endl; …ブロックの出口で所有権を失う。メモリ領域も解放される }
では、このブロック内で別のスマートポインタを宣言して、初期値をu_str_ptrにしてみます。代入なので、所有権がコピーされておのおのが異なるメモリ領域を保持するか、あるいは所有権を共有するかのような動きをするように見えますが、実際はコンパイルエラーとなります。それは、unique_ptrがコピーコンストラクタもコピー代入演算子も持っておらず、所有権のコピーを認めていないからです。
{ auto u_str_ptr = make_unique<string>("Hello, World!!"); auto u_str_ptr2 = u_str_ptr; // 代入はコンパイルエラー }
スマートポインタunique_ptrにとって、所有権はムーブの対象です。所有権をムーブするには、紹介済みのstd::move関数を使います。move関数によってu_str_ptrが右辺値にキャストされるので、所有権がu_str_ptrからu_str_ptr2にムーブします。u_str_ptrはnullptrとなり、表示させようとするとsegmentation faultなどのエラーとなります。その替わり、新たに所有権を得たu_str_ptr2の表示は問題なく行われます。
{ auto u_str_ptr = make_unique<string>("Hello, World!!"); auto u_str_ptr2 = std::move(u_str_ptr); cout << *u_str_ptr2 << endl; cout << *u_str_ptr << endl; // segmentation fault }
所有権を共有できるスマートポインタ[C++11]
スマートポインタには、所有権を共有できるshared_ptrがありました。std::make_shared<T>関数で作成します。あくまでも共有で、メモリ領域を複製するわけではありません。
auto s_str_ptr = make_shared<string>("Hello, World!!"); auto s_str_ptr2 = s_str_ptr; cout << *s_str_ptr << endl << *s_str_ptr2 << endl; // 問題なく2つ表示される *s_str_ptr = "Hello, C++ World!!"; cout << *s_str_ptr << endl << *s_str_ptr2 << endl; // 双方とも書き換わる
smartptr.cppでは代入時にコンパイルエラーとなっていましたが、shared_ptrでは問題なく表示まで実行されています。shared_ptrはコピーコンストラクタとコピー代入演算子を持っているので、代入によって所有権のコピーが実行され、s_str_ptrとs_str_ptr2が同じメモリ領域を所有するものとしてマークされます。片方を変更したら、もう一方も変更されます。
shared_ptrでも所有権はmove関数を使ってムーブできます。以下のリストでは、s_str_ptrの所有権をs_str_ptr3にムーブしています。ムーブ後でも、s_str_ptr2の所有権は生きていることに注目です。もちろん、s_str_ptrはムーブで無効になっているので表示させようとすると実行時にエラーとなります。
…上記の続き… auto s_str_ptr3 = std::move(s_str_ptr); cout << *s_str_ptr2 << endl << *s_str_ptr3 << endl; // 問題なく2つ表示される cout << *s_str_ptr << endl; // segmentation fault
共有スマートポインタは、簡単に言うと所有権を持っている変数(左辺値といった方がそれっぽいですね)が幾つあるのか、その情報を持っています。要はカウンタですね。カウンタが0以上である限りメモリ領域を保持し、0になればメモリ領域を解放します。このように、データ本体のためのメモリ領域とは別にカウンタの保持、操作が必要なので、若干メモリを食いますし、生成時や代入時の実行コストも少し増えます。
まとめ
今回は、Modern C++のキモとも言えるムーブセマンティクスを紹介しました。とっつきにくい用語ですが、ムーブという考え方をC++に導入し、コード実行の効率化を言語仕様として明確にものだというのをお伝えできたのではないかと思います。
次回は、いよいよC++でも使えるようになったラムダ式と無名関数を紹介します。