C++11以降のC++はModern C++と言われ、それ以前のC++(古典的C++)に対して多くの言語仕様が追加された。古典的C++しか知らないプログラマがModern C++の言語仕様を使い倒したコードを見ても、さっぱり分からないかもしれない。
古典的C++を主戦場とするプログラマに対して、盲目的にModern C++に移行すべきだと言うつもりはない。ただ、Modern C++は古典的C++の上位互換であり、古典的C++の枠組みで書かれたコードに対してModern C++の一部の仕様をつまみ食い的に利用するだけでも、おいしいことはある。
そこで、古典的C++の枠組みでコードを書くプログラマがつまみ食いする価値のあるModern C++の仕様について、いくつかの記事に分けて紹介していく。
ムーブセマンティクス
Modern C++で追加された主な言語仕様の一つに、右辺値参照を利用したムーブセマンティクスがある。右辺値参照は、一時オブジェクトなど「もう使い回さなくてよい値」を受けるための参照で、それを使って「コピーせずに中身を移す」ための仕組みがムーブセマンティクスである。(右辺値参照およびムーブセマンティクスの仕様ついては、「右辺値参照・ムーブセマンティクス [N2118]」を参照)
ムーブセマンティクスを活用することで、不要なデータのコピーを省いてプログラムを高速化できる。特に、今回例として紹介するシーケンスコンテナ(std::vector)に要素として文字列(std::string)を追加していく処理では、古典的C++の場合と同じプログラムコードでムーブセマンティクスの恩恵にあずかることができる。ユースケースとしては、例えば、ログメッセージのようなデータをシーケンスコンテナに動的に追加していく処理が考えられる。
ムーブセマンティクスによる動作効率の改善例
以下の題材で、コンパイル時にModern C++の仕様を有効化するかどうかで処理時間にどれくらい差が生じるか実測した。
プログラムの内容
std::vectorに文字列(std::string)をpush_backで追加していく処理の繰り返しである。
#include <vector>
#include <string>
static const int MAIN_LOOP = 100000;
static const int MAX_VECTOR_SIZE = 20;
static const int STR_LEN = 300;
static const char CHR = 'a';
//#define DO_NOTHING
std::size_t PushBack(std::vector<std::string>* v) {
for (int i = 0; i < MAX_VECTOR_SIZE; ++i) {
std::string s(STR_LEN, CHR);
#ifndef DO_NOTHING
v->push_back(s);
#endif // not DO_NOTHING
}
return v->size();
}
std::size_t ret = 9999; // Dummy variable
int main() {
for (int i = 0; i < MAIN_LOOP; ++i) {
std::vector<std::string> v;
ret = PushBack(&v);
}
return 0;
}
コンパイルにはGCCを用い、ムーブセマンティクスを用いない場合はコンパイルオプションとして--std=c++03を指定し、ムーブセマンティクスを用いる場合は–std=c++11を指定する。
コンパイル時にマクロ DO_NOTHING を定義するとシーケンスコンテナ(std::vector)へのpush_backが行われないようになる。これは、プログラムの起動やループを回すなど、シーケンスコンテナへのpush_back以外の処理にどれくらい時間がかかっているかの目安を測定する場合に用いる。
プログラム中のv->push_back(s)について補足する。C++03では、push_back(s)は文字列のコピーである。C++11では、v->push_back(std::move(s))に相当する。C++11では、std::vector内部で要素を保持・再配置する際に std::stringのムーブが使われ、文字列バッファ本体をコピーせずに所有権を移譲するからである。
測定方法
測定パターンは、プログラムの動作としては以下の3通りで、それぞれコンパイル時の最適化なし(-O0)と最適化あり(代表して-O2)の計6通り(3×2)である。
- ムーブセマンティクスを利用しない場合(古典的C++
--std=c++03) - ムーブセマンティクスを利用する場合(Modern C++
--std=c++11) - push_backをしない…上の二つの所要時間からこの所要時間を引くことで、push_backにかかっている正味の時間の目安となる
実行時の所要時間は以下のように測定した。
- timeコマンドで得られるユーザCPU時間とシステムCPU時間の合計を処理時間とみなした
- 一つの測定パターンにつき100回測定し、それぞれのパターンについて、結果の上位10個と下位10個を除いた80回の平均を求めた(結果の上位と下位10個ずつを除く理由は、偶発的に良い結果や悪い結果が出た場合の影響を避けるためである)
- 動作環境はUbuntu26.04(x86_64版)、GCCはバージョン15.2.0を用いた
測定結果
それぞれの測定パターンでの所要時間は、以下のような結果になった。
| ムーブセマンティクスを利用しない(古典的C++) | ムーブセマンティクスを利用する(Modern C++) | std::vectorへのpush_backなし | |
|---|---|---|---|
| 最適化なし | 3.084[秒] | 2.547[秒] | 0.649[秒] |
| 最適化あり | 0.928[秒] | 0.500[秒] | 0.170[秒] |
push_backなしでの所要時間の目安(上の表の最も右の列の値を引いたもの)は以下のようになる。
| ムーブセマンティクスを利用しない(古典的C++) | ムーブセマンティクスを利用する(Modern C++) | |
|---|---|---|
| 最適化なし | 2.435[秒] | 1.898[秒] |
| 最適化あり | 0.758[秒] | 0.330[秒] |
上記結果のように、ムーブセマンティクスを利用することで所要時間は明らかに削減できている。ムーブセマンティクスを利用しない場合と比べて文字列のデータコピーが発生しないのだから当然である。
ムーブセマンティクスを利用するためにプログラムを書き分けるのではなく、同一のソースコードでコンパイル時にModern C++の言語仕様を有効化するだけで実現できているのがうれしい。
まとめ
今回はあえて同一のソースコードでコンパイルオプションを違えるだけでムーブセマンティクスを活用できる例で試したが、ムーブセマンティクス向けのコードを書く必要があるとしても、不要なデータコピーを抑えて処理のオーバーヘッドを減らす価値はあるのではないだろうか。
自前で実装するクラスにおいては、ムーブ後もオブジェクトを破棄可能な妥当状態に保つことや、メモリ領域を所有権移譲後に二重解放しないことを考慮して、ムーブコンストラクタやムーブ代入演算子、デストラクタを定義するのが基本だろう。