yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

gistにソースコードを放っておいたらOSSプロジェクトで活用されていた件

タイトル通り。

プログラミング言語Cの最新規格C11で採用されたスレッドサポートのエミュレーションライブラリをgistに放置していたら、汎用OpenGL実装The Mesa 3D Graphics Libraryに取り込まれました(2014年2月現在のGit版として)。

2014-05-07追記:MesaLib-10.1.0からinclude/c11以下にとりこまれたようです。

対応プラットフォームが多岐にわたるMesa 3Dでは、その差異を吸収するスレッドサポート実装をもっていましたが、4つのサブモジュールでそれぞれ独立に行われていたそうです。つまり、4種類のエミュレーション用コードがばらばらに存在する状態でした。そこで、C11標準ライブラリのAPI仕様に則りかつWindowsPOSIX環境に対応していたgistのコードが拾われて、プロジェクト内の重複実装を整理する目的で採用されたようです。

ライブラリ内部実装としては、Pthreadの薄っぺらいラッパとBoost.InterprocessライブラリC++言語)からC言語への移植です。作成動機はC11標準スレッドサポート機能の調査と、条件変数プリミティブの実装調査でした。

(コードをBoostライセンスで公開していたのもあってか、Phoronix記事ではBoostライブラリの一部と勘違いされている?)

ま、そういうOSS貢献(?)の形もあったという事で一つ。

スレッドセーフという幻想と現実

この記事はC++ Advent Calendar 2013の15日目にエントリしています。
内容はC++標準ライブラリとスレッドセーフに関する解説になります。

f:id:yohhoy:20111211120104j:plain
flickr / rennasverden
もくじ

  1. What's スレッドセーフ?
    1. スレッドセーフという幻想
    2. 基本型とデータ競合
    3. C++標準ライブラリとデータ競合
    4. C++標準ライブラリ:シーケンスコンテナ編
    5. C++標準ライブラリ:連想コンテナ編
  2. スレッドセーフ RELOADED
    1. 基本的なスレッドセーフ保証
    2. std::shared_ptr<T>
    3. std::rand()
    4. std::cout

(本文のみ約9000字)

はじめに

マルチスレッド対応の点では他言語に遅れを取っていたプログラミング言語C++ですが、C++11ではようやく標準ライブラリにスレッドサポートが追加されました。C++11スレッドサポートではスレッドクラスstd::threadをはじめとし、ミューテックスstd::mutexや条件変数std::condition_variableなどの基本的な同期機構、Future/Promiseパターンを実現するstd::future, std::promise、少しマニアックなところではアトミック変数std::atomicなどがC++標準ライブラリに追加されました。Happy Multithreading Programming!

という風に、C++11スレッドサポートの文脈では新規クラス追加ばかりが注目されています。このような華やかな表舞台の裏側では、以前からあるstd::stringstd::vectorといったクラス群に対して、スレッドサポートに関連する重要なルールが明確化されたことは知っていますか?マルチスレッドプログラム中でC++標準ライブラリを利用するとき、それらのスレッドセーフ性(thread safety)*1について理解し、適切に使えているでしょうか?

本記事では、プログラミング言語C++におけるスレッドセーフの観点から、C++11スレッドサポート追加クラス以外C++標準ライブラリをいくつか紹介したいと思います。その中でも、たまに論争ネタになっているstd::shared_ptrstd::coutのスレッドセーフ性についても説明を加えていきたいと思います。

1. What's スレッドセーフ?

複数スレッドを扱うプログラミングについて学び始めると、必ず「スレッドセーフ(thread safe)」という概念が出てくると思います。曰く、

  • △△ライブラリはスレッドセーフだから、マルチスレッドプログラム中でも安全に使える。
  • クラス○○はスレッドセーフじゃないから、スレッドと一緒には使えない。

どうやら“スレッドセーフなC++ライブラリ=マルチスレッド対応”という雰囲気のようです。スレッドセーフなんて簡単ですね!

…でも、本当にそんなに単純な話でしょうか。次の「スレッドセーフ」に関する質問に答えられますか?(Q&Aサイトでよく見かける質問の変形です)

  • Q1. std::map<K,V>クラスはスレッドセーフですか?
  • Q2. std::vector<T>クラスはスレッドセーフですか?
  • Q3. std::stringクラスはスレッドセーフですか?
  • Q4. 基本型intはスレッドセーフですか?
  • Q5. そもそもC++言語での「スレッドセーフ」は何を意味しますか?

これらに回答できるなら、プログラミング言語C++の文脈での「スレッドセーフ」という概念について、ちゃんと理解していると言っても差し支えないでしょう。本記事の前半では、逆順にてそれぞれの質問に答えていきたいと思います。

1.1. スレッドセーフという幻想

Q5. そもそもC++言語での「スレッドセーフ」は何を意味しますか?

いきなり出鼻をくじくようですが、プログラミング言語C++の言語仕様としては「スレッドセーフ」という用語は直接定義されていません。ただし、スレッドセーフを考える上で重要な用語「データ競合(data race)」は、C++言語仕様の一部として厳密に定義されます。C++11におけるデータ競合とは、次の3条件を全て満たすときに発生します。*2

  • (1) 同一メモリ位置に対するアクセスにおいて、
  • (2) 少なくとも一方が変更(modify)操作であり、
  • (3) 異なるスレッド上から同時に行われるとき。

f:id:yohhoy:20131211204648j:plain

そして、C++言語仕様では「プログラム中で何らかのデータ競合が発生した場合には、未定義の動作(undefined behavior)を引き起こす」と明言しています。つまり、ひとつでもデータ競合を含むマルチスレッドプログラムの動作結果について、C++言語仕様としては何も保証しないことを意味します。ここでの“何も保証しない”とは、本当に何も保証されていことに注意してください。例えば、データ競合を含むプログラムは偶然正しく動くかもしれないし、運悪く期待する結果とならないかもしれないし、はたまたプログラム実行中に突然異常終了するかもしれないという意味です。*3

残念ながら原典をあたっても「スレッドセーフ」についての明確な答えは得られませんでしたが、少なくともデータ競合を含まないプログラムとなっていればよい事は分かりました。

Q5. そもそもC++言語での「スレッドセーフ」は何を意味しますか?
A5. 厳密には「スレッドセーフ」は定義されない。一方、妥当なC++プログラムはデータ競合を一切含まないことが求められる。

と、このままでは話が終わってしまいますので、厳密なC++言語仕様から少し離れてみたいと思います。ある対象の「スレッドセーフ」について言及するときは、その利用者視点から議論するのが一般的と考えられます。本記事でもそれにならい“利用側コードからどのように扱えばデータ競合が生じないか”、より具体的には“いつ排他制御すれば良いのか”という観点から、基本型やクラスの「スレッドセーフ」の議論を続けていきます。

1.2. 基本型とデータ競合

Q4. 基本型intはスレッドセーフですか?

charintなどの基本型*4は、C++プログラムを構成する最も原始的なデータ構造です。このような基本型変数は、一定サイズのメモリ領域にマッピングされます(例:sizeof(int) == 4な環境ではint型変数は連続した4バイトメモリ位置を占める)。つまり、基本型に関するスレッドセーフの議論は、前述「データ競合」の定義から単純に読み替えが可能です。

ところで「データ競合」は3つの発生要因から構成されますが、逆に言うと、これらの要因のうち1つでも排除できれば回避できます。

  • (1') 異なる変数に対する同時アクセスは、データ競合とはなりません。
  • (2') 同一変数に対して同時読み込み(read)操作のみならば、データ競合とはなりません。
  • (3') 同一変数に対するアクセスが同時でなければ、データ競合とはなりません。

(1')はアクセスする変数自体が別々となるため、最も直感的に理解できるかと思います。(以降のコード例では、関数th1(), th2()等は異なるスレッドからそれぞれ同時に呼び出されるとします。)

// 異なる変数に対する同時アクセス
int x, y;
void th1() {
  x = 1;  // OK
}
void th2() {
  y = 2;  // OK
}

また(3')はマルチスレッドプログラムの定番、ミューテックスstd::mutexなどを用いた排他制御により同一変数へのアクセスが同時に生じないよう制御します。これも、マルチスレッドプログラミングの基本ですね。

// 同一変数に対するアクセスが同時でない
std::mutex mtx;
int x;
void th1() {
  std::lock_guard<decltype(mtx)> lk(mtx);
  x = 1;  // OK
}
void th2() {
  std::lock_guard<decltype(mtx)> lk(mtx);
  x = 2;  // OK
}

一番馴染みが薄いのは(2')でしょうか?C++における「データ競合」の定義に従うと、同一変数に対する読み込み操作のみであれば安全に同時アクセスが可能なのです(変数x)。ただし、1つでも変更操作が発生するケース(変数y)では、データ競合を回避するために排他制御が必要となることに注意してください。

// 同一変数に対して同時読み込み操作のみ
int x = 0;
void th1() {
  int r1 = x; // OK
}
void th2() {
  int r2 = x; // OK
}
// 同一変数に対して片方が変更操作
int y = 2;
void th1() {
  int r2 = y; // NG: データ競合
}
void th2() {
  y = 42;     // NG: データ競合
}

利用者視点で考えると、上記ルール“全てが読み込み操作ならば排他制御は不要である”というのが、C++基本型が提供するスレッドセーフ保証と解釈できます。

Q4. 基本型intはスレッドセーフですか?
A4. 「同時アクセスが全て読み込み操作であれば安全」というスレッドセーフ性レベルが保証される。

1.3. C++標準ライブラリとデータ競合

Q3. std::stringクラスはスレッドセーフですか?

さて基本型のスレッドセーフ保証は分かりましたが、C++標準ライブラリ提供のクラスではどうでしょう。例えば文字列型std::stringクラスでは、明らかにprivateなメンバ変数として複数の基本型(文字列ポインタやバッファ長など)を内包しているはずです。一方でC++11以前のプログラム動作との互換性を考慮すると、クラス内部実装にミューテックスなど排他制御の仕組みがこっそり追加されたとは考えにくいですね。そうなると、マルチスレッドプログラムからこれらのクラスを安全に利用するには、利用者側にて全てのメンバ関数呼び出しを排他制御しないとダメなのでしょうか…?

std::string s = "hello";
void th1()
{
  // ここに排他制御は必要?
  bool b = s.empty();
}
void th2()
{
  // ここに排他制御は必要?
  char& c = s.at(0);
}

実は、C++11における重要な(そして地味な)スレッドサポートとして、C++標準ライブラリ提供クラスのデータ競合に関する基本ルールが明確化されました。

  • クラスオブジェクトのconstメンバ関数呼び出しは、オブジェクトに対する読み込み操作とみなす。
  • クラスオブジェクトのconstメンバ関数呼び出しは、オブジェクトに対する変更操作とみなす。(一部例外事項あり)

このルールはC++標準ライブラリ提供のクラスに対して適用される、最低限かつ基本的なスレッドセーフ保証となっています。また追加的な例外事項として、begin, endatなどの一部の非constメンバ関数ではオブジェクト自身の内部状態を変更しないため、これらもデータ競合に関してはconstメンバ関数相当(=読み込み操作)とみなします。

std::string s = "hello";
void th1()
{
  if ( !s.empty() )             // OK: constメンバ関数呼び出し
    printf("%s\n", s.c_str());  // OK: constメンバ関数呼び出し
}
void th2()
{
  size_t n = s.length();  // OK: constメンバ関数呼び出し
  char& c = s.at(0);      // OK: constメンバ関数相当呼び出し
}

つまり、データ競合を回避する3つの方法は次のように拡張できます。

  • (1') 異なるオブジェクトに対する同時アクセス/メンバ関数呼び出しは、データ競合とはなりません。
  • (2') 同一オブジェクトに対して同時読み込み操作=constメンバ関数相当呼び出しのみならば、データ競合とはなりません。
  • (3') 同一オブジェクトに対するアクセス/メンバ関数呼び出しが同時でなければ、データ競合とはなりません。

こちらも利用者視点の表現では、上記ルール“全てconstメンバ関数相当呼び出しならば排他制御は不要である”というのが、C++標準ライブラリが提供するスレッドセーフ保証と解釈できます。

Q3. std::stringクラスはスレッドセーフですか?
Q3. 「同時アクセスが全て読み込み操作(constメンバ関数相当呼び出し)であれば安全」というスレッドセーフ性レベルが保証される。

1.4. C++標準ライブラリ:シーケンスコンテナ編

Q2. std::vector<T>クラスはスレッドセーフですか?

他の型を内包するシーケンスコンテナ(sequence containers)クラスはどうでしょうか(説明のためT=intとする)。コンテナクラスのデータ競合を考える場合、コンテナオブジェクト(vector<int>)と格納された各要素(int)は異なるオブジェクトであると認識することが重要です。

コンテナ要素へアクセスする最も一般的な方式はvector<T>::operator[]メンバ関数呼び出しでしょう。このoperator[]にはconst・非constメンバ関数の2つが存在しますが、前掲の「基本的なスレッドセーフ保証」により両者ともconstメンバ関数(相当)として扱われます。このため、コンテナオブジェクトの他constメンバ関数と同時に呼び出してもデータ競合とはなりません。

std::vector<int> v = { 1, 2, 3, 4, 5 };
void th1()
{
  size_t n = v.size();  // OK: v.size()は、vに対する読み込み操作
}
void th2()
{
  v[1] *= 2;  // OK: v[1]つまりv.operator[](1)は、vに対する読み込み操作
  // v[1] *= 2は、vの2番目要素に対する変更操作
}

operator[]や他メンバ関数イテレータ経由やatなど)を介して得られた各コンテナ要素オブジェクトへのアクセスについては、既に説明してきたデータ競合の考え方と同じになります。つまり、異なるコンテナ要素オブジェクトへのアクセスであれば、同時に変更操作を行ってもデータ競合とはなりません。((このコンテナ要素アクセスに関するデータ競合に関して、1つだけ例外ケースが存在します。特殊化されたstd::vector<bool>だけは、異なる要素へのアクセスであってもデータ競合が発生する可能性があります。同テンプレート特殊化版の仕様が要請するメモリ空間最適化のために、異なるboolオブジェクトが“同一メモリ位置(バイト)”の異なるビットとして配置される可能性があるためです。))

std::vector<int> v = { 1, 2, 3, 4, 5 };
void th1()
{
  // v[1]つまりv.operator[](1)は、vに対する読み込み操作
  ++v[1];    // OK: vの2番目要素に対する変更操作
}
void th2()
{
  // v[3]つまりv.operator[](3)は、vに対する読み込み操作
  v[3] = 0;  // OK: vの4番目要素に対する変更操作
}

f:id:yohhoy:20131211204717j:plain

Q2. std::vector<T>クラスはスレッドセーフですか?
A2. コンテナ自身std::vector<T>および各要素Tに対して、個別に「同時アクセスが全て読み込み操作(constメンバ関数相当呼び出し)であれば安全」というスレッドセーフ性レベルが保証される。

1.5. C++標準ライブラリ:連想コンテナ編

Q1. std::map<K,V>クラスはスレッドセーフですか?

最後に、より複雑な連想コンテナ(associative containers)クラスをみていきましょう(説明のためK=int, V=std::stringとする)。既に説明してきた通り、コンテナオブジェクト(map<int,string>)と格納されるキー(int)-値(string)をそれぞれ異なるオブジェクトと考える必要があります。これらのクラス/型はそれぞれで基本的なスレッドセーフ保証を提供しますから、もう説明しなくても分かりますよね。

std::map<int,std::string> m = { {1,"a"}, {2,"b"} };
void th1()
{
  size_t n = m.size();  // OK: m.size()は、mに対する読み込み操作
}
void th2()
{
  m.at(2) += "x";  // OK: m.at(2)は、mに対する読み込み操作
  // m.at(2) += "x"は、mの要素{2,"b"}の値に対する変更操作
}

ところで、上記コードでは意図的にmap<int,string>::operator[]の利用を避けました。実は“連想コンテナクラスのoperator[]メンバ関数は変更操作”と解釈するため、下記コードへ単純に置き換えるとデータ競合を引き起こしてしまうのです((連想コンテナstd::map, std::unordered_mapでは非constoperator[]メンバ関数のみが提供され、そもそもconstoperator[]メンバ関数は存在しません。))。少々不便に見えるかもしれませんが、このメンバ関数は“与えられたキー値が存在しなければ新要素を追加する”という動作仕様となっており、これはコンテナ自身の内部状態を変更しないことには実現できないからです。

std::map<int,std::string> m = { {1,"a"}, {2,"b"} };
void th1()
{
  size_t n = m.size();  // NG: データ競合
}
void th2()
{
  m[2] += "x";  // NG: データ競合
  // m[2]つまりm.operator[](2)は、mに対する変更操作
}

つまりmap<int,string>::operator[]を安全に使うには、ミューテックスによる排他制御が必要となるのです。

std::mutex mtx;
std::map<int,std::string> m = { {1,"a"}, {2,"b"} };
void th1()
{
  std::lock_guard<decltype(mtx)> lk(mtx);
  size_t n = m.size();  // OK
}
void th2()
{
  std::lock_guard<decltype(mtx)> lk(mtx);
  m[2] += "x";  // OK
}

C++標準ライブラリでは、連想コンテナstd::map<K,V>, std::unordered_map<K,V>の2つで本ルールが当てはまります。

Q1. std::map<K,V>クラスはスレッドセーフですか?
A1. コンテナ自身std::map<K,V>および各要素キーKと値Vに対して、別個に「同時アクセスが全て読み込み操作(constメンバ関数相当呼び出し)であれば安全」というスレッドセーフ性レベルが保証される。ただしoperator[]はコンテナに対する変更操作であることに注意。

2. スレッドセーフ RELOADED

いかがでしょう。5つの質問には答えられましたか?ここでは、改めてC++標準ライブラリの基本的なスレッドセーフ保証について整理し、またこの考え方が特段目新しいものでないことを紹介します。さらに、しばしばスレッドセーフ論争を引き起こすクラスや関数をいくつか挙げて、追加的な解説を行いたいと思います。

2.1. 基本的なスレッドセーフ保証

前節の繰り返しとなりますが、C++言語仕様およびC++標準ライブラリでは次の「基本的なスレッドセーフ保証」を提供します。これは、C++標準ライブラリ提供のクラス/関数が提供する、最低限のスレッドセーフ性の保証となっています。ちなみに、ここで“最低限の”と表現しているのは、C++標準ライブラリの一部クラスではもっと強いスレッドセーフ保証(=同時に変更操作を行っても安全)を提供するためです。

  • 同一オブジェクトにする同時アクセスが全て読み込み操作であれば、排他制御なしでもデータ競合を引き起こさない。
  • C++標準ライブラリ提供のクラスでは、constメンバ関数(およびオブジェクトを変更しない非constメンバ関数)呼び出しは読み込み操作とみなす。
  • これ以外のケース(少なくとも片方が変更操作)の場合、同一オブジェクトへの同時アクセスを排他制御する必要がある。

さて、この「基本的なスレッドセーフ保証」ですが、実はC++11で新たに作られた考え方ではありません。C++98以前から存在しており、また現C++標準ライブラリの源流でもあるSGI STLライブラリでも、同等のスレッドセーフ保証を提供すると明言していました。*5

The SGI implementation of STL is thread-safe only in the sense that simultaneous accesses to distinct containers are safe, and simultaneous read accesses to to shared containers are safe. If multiple threads access a single container, and at least one thread may potentially write, then the user is responsible for ensuring mutual exclusion between the threads during the container accesses.

Standard Template Library Programmer's Guide, Thread-safety for SGI STL

さらにC言語の時代にまでさかのぼると、POSIXシステムにおいても同等のスレッドセーフ保証が明言されていました。(こちらは対象がC言語のためconstメンバ関数は存在しません。)

(略)They usually cannot determine when memory operation order is important and generate the special ordering instructions. Instead, they rely on the programmer to use synchronization primitives correctly to ensure that modifications to a location in memory are ordered with respect to modifications and/or access to the same location in other threads. Access to read-only data need not be synchronized. The resulting program is said to be data race-free.

The Open Group Base Specifications Issue 6, IEEE Std 1003.1, 2004 Edition

2.2. std::shared_ptr<T>

C++11では所有権共有スマートポインタstd::shared_ptr<T>C++標準ライブラリ入りしました。ここでは同一intオブジェクトを参照する2つのスマートポインタが存在し、同時にp1, p2を操作するケースを考えてみます(説明のためT=intとする)。各スレッド上での操作により参照カウント更新が同時に行われるため、これまでの延長線上で考えると排他制御が必要に思えます。

std::shared_ptr<int> p1 = std::make_shared(42);
std::shared_ptr<int> p2 = p1;
// p1とp2は同一オブジェクトを参照
void th1() {
  // ここに排他制御は必要?
  p1.reset();   // 参照カウントを-1
}
void th2() {
  // ここに排他制御は必要?
  auto q = p2;  // 参照カウントを+1
}

一方で、局所的な観点ではp1p2はあくまで“異なるstd::shared_ptr<int>オブジェクト”です。仮に両スマートポインタが異なるintオブジェクト/異なる参照カウントを参照するなら、C++標準ライブラリの基本的なスレッドセーフ保証より明らかに排他制御は不要です。とはいえ、その参照先が同一か否かで必要有無が変わるようでは、結局は保守的に排他制御をせざるを得なくなります。

std::shared_ptr<int> p1 = /* ??? */;
std::shared_ptr<int> p2 = /* ??? */;
// p1とp2は同一オブジェクトor異なるオブジェクトを参照
void th1() {
  // ここに排他制御は必要?
  p1.reset();   // 参照カウントを-1
}
void th2() {
  // ここに排他制御は必要?
  auto q = p2;  // 参照カウントを+1
}

この問題に対して、C++標準ライブラリでは「異なるstd::shared_ptrオブジェクトに対するメンバ関数呼び出しは、その参照先に関わらずデータ競合を引き起こさない」と明確に保証しています。つまり、利用者からは見えないオブジェクト内部状態を気にすることなく、基本的なスレッドセーフ保証の通り“異なるオブジェクトへの同時アクセスは安全”と扱ってして良いのです。これは裏を返すと、スマートポインタstd::shared_ptrの内部実装では、参照カウント更新はatomic変数操作(または排他制御あり変数更新)にて行われることを意味します。

std::shared_ptr<int> p1 = std::make_shared(42);
std::shared_ptr<int> p2 = p1;
// p1とp2は同一オブジェクトを参照
void th1() {
  p1.reset();   // OK: 参照カウント-1は安全に行われる
}
void th2() {
  auto q = p2;  // OK: 参照カウント+1は安全に行われる
}

f:id:yohhoy:20131211204745j:plain

ただし、両スマートポインタp1, p2が同一オブジェクトを参照している状況で、shared_ptr<int>::operator*メンバ関数を介して得られた参照先intオブジェクトにアクセスする時には、intに対する基本的なスレッドセーフ保証に留意する必要があります。

std::shared_ptr<int> p1 = std::make_shared(42);
std::shared_ptr<int> p2 = p1;
// p1とp2は同一オブジェクトを参照
void th1() {
  *p1 += 1;     // NG: 参照先においてデータ競合
  // 参照先オブジェクトに対する変更操作
}
void th2() {
  int r = *p2;  // NG: 参照先においてデータ競合
  // 参照先オブジェクトに対する読み込み操作
}
std::shared_ptr<int> p1 = std::make_shared(42);
std::shared_ptr<int> p2 = p1;
// p1とp2は同一オブジェクトを参照
void th1() {
  int r = *p1;  // OK: 参照先オブジェクトに対する読み込み操作
}
void th2() {
  int r = *p2;  // OK: 参照先オブジェクトに対する読み込み操作
}

2.3. std::rand()

続いて、C言語時代の標準ライブラリから引きついだ乱数生成関数std::rand()を取り上げてみましょう((例示コードでは整数0~9を得るために剰余演算(%)を利用していますが、この実装では望ましい一様分布とはなりません。定数RAND_MAXを利用すればもう少しまともな一様分布が得られますが、C++11以降では後述の乱数ライブラリ<random>を利用すべきです。))。C++11では、この関数を複数スレッドから同時に呼び出した場合、データ競合を引き起こすか否かは処理系定義(implementation-defined)と定めています。つまり、コンパイラやライブラリに任せるとしか言っていないのです。

void th1() {
  // 処理系によっては排他制御が必要
  int r1 = std::rand() % 10;
}
void th2() {
  // 処理系によっては排他制御が必要
  int r2 = std::rand() % 10;
}

(スレッドセーフの議論に関わらず)C++11以降では、C++標準ライブラリに追加された乱数ライブラリを用いるべきでしょう。この新しい乱数ライブラリ<random>であれば乱数生成器をスレッド別に持つことができるため、データ競合について悩む必要が無くなります。((例示コードでは乱数生成エンジンstd::mt19937を引数なしで構築しているため、常に同一の疑似乱数列が得られることに注意してください。実行のたびに異なる乱数列が必要な場合、std::random_deviceなどで適当なシード値を与える必要があります。))

void th1() {
  std::mt19937 rng;
  std::uniform_int_distribution<int> dist(0, 9);
  int r1 = dist(rng);  // OK
}
void th2() {
  std::mt19937 rng;
  std::uniform_int_distribution<int> dist(0, 9);
  int r2 = dist(rng);  // OK
}

もちろん、下記コードのように単一の乱数生成器を複数スレッド間で共有し、アクセス時には排他制御を行うという実装でもOKです。ただし、この設計では“(a)高頻度で乱数生成を行う状況で、排他制御により他スレッドが停止するためプログラム処理速度に悪影響を与える”、“(b)各スレッドで取得する乱数列は実行時(OS)スレッドスケジューリングに依存するため、再現性のある乱数列を生成できない”という問題が生じます。最終的にはアプリケーションの目的によりますが、一般にはスレッド別に乱数生成器を保持する設計の方が好ましいと思います。

std::mutex mtx;
std::mt19937 rng;
std::uniform_int_distribution<int> dist(0, 9);
// 単一の乱数生成器を排他制御ありで利用
int next_rand() {
  std::lock_guard<decltype(mtx)> lk(mtx);
  return dist(rng);
}

void th1() {
  int r1 = next_rand();  // OK
}
void th2() {
  int r2 = next_rand();  // OK
}

今回はrand()関数をとりあげましたが、C++標準ライブラリに取り込まれたC標準ライブラリの関数のうち、strtok(), localtime(), setlocale()などの一部関数は複数スレッドから同時に呼び出すとデータ競合となる可能性があります。できるだけC++標準ライブラリを使いましょう!

2.4. std::cout

最後に、おそらく一番論争を引き起こすであろう、標準出力ストリームstd::coutのスレッドセーフ保証について整理します。C++標準ライブラリの基本的なスレッドセーフ保証に照らして考えると、各スレッドから同時にstd::coutへ出力するとき排他制御は必要でしょうか?

void th1() {
  // ここに排他制御は必要?
  std::cout << "I'm #1 thread." << std::endl;
}
void th2() {
  // ここに排他制御は必要?
  std::cout << "Hello, Multithreading World!" << std::endl;
}

この質問に対する回答は、2段階で考える必要があるでしょう。まず、C++11の標準ライブラリ仕様では「標準入出力ストリームオブジェクトに対する入力/出力操作は、排他制御を行わなくてもデータ競合を引き起こさない」と定めています。という訳で、上記コードはデータ競合なしに正常動作することが保証されています。つまり、標準入出力ストリームオブジェクトstd::cin, std::cout, std::cerr, std::clog(と対応するwide文字版)では、C++標準ライブラリの基本的なスレッドセーフ保証よりも強いスレッドセーフ性を提供しています。((このような強いスレッドセーフ保証が行われるのは、あくまで標準入出力ストリームオブジェクトのみです。std::fstreamstd::stringstreamでは、基本的なスレッドセーフ保証しか提供しないのでご注意を。))

ただし、C++標準ライブラリでは「データ競合は起こさないが、複数スレッドからの出力が混ざる(interleave)可能性がある」とも言及しています。例えば標準出力には次のような文字列が出力されるかもしれませんが、これもプログラムが正常動作した結果なのです。

I'm #1
Hello,thread Multit
hreading World!

一般的には前掲コードを書いたプログラマが期待するのは、下記いずれかのように“行単位で出力されること”と考えられます。結局のところ標準出力ストリームstd::coutであっても、実用上は排他制御を行わない限りはまともな出力結果が得られないのです。

I'm #1 thread.
Hello, Multithreading World!
Hello, Multithreading World!
I'm #1 thread.

まとめ

これで今回の解説はおしまいです。最後にもう一度、本記事での要点をまとめてみましょう。

  • C++11における基本型とC++標準ライブラリ提供クラスでは、少なくとも下記「基本的なスレッドセーフ保証」を提供します。
  • 単一オブジェクトに対する同時アクセスが、全て“読み込み操作”=“const関数相当呼び出し”のみであれば、複数スレッド間での排他制御は不要です。
  • このルールは、複数オブジェクト間で内部共有されるデータがあったとしても維持されます。(例:std::shared_ptr
  • std::cout等の標準入出力ストリームを使う場合も、複数スレッド間での排他制御が必要になります。

I Hope You Have A Nice Multithreading C++ Programming Life!!

C++ Advent Calendar 2013 は 16日目 disktnk さんの記事へと続きます。
f:id:yohhoy:20131215154349j:plain
flickr / applebymatt

参考記事

本文中では次の記事をもとにした解説を行いました。より詳細についてはそれぞれの記事もご参考にください。

はしがき:本文中でゆるふわ手書き画像を使っていますが、当初はInkscapeで描こうと思いつつ心が折れた結果です。字が汚いことを横に置けば、手書きも悪くないですね!

*1:本記事中では thread safe=スレッドセーフ との対比から thread safety=スレッドセーフ性 と記述します。なお、後者の対訳語としては「スレッド安全性」の方が一般的かと思います。

*2:厳密にはもう1つの条件“(0) 対象がatomic変数でないとき”が追加されます。これを裏返すと、atomic変数に対するアクセスはデータ競合を引き起こさないことを意味します。本文中ではatomicでない通常の変数のみを対象とするため、この条件については言及していません。

*3:「データ競合」によるプログラム異常終了というのは直感に反するかもしれませんが、これは「未定義の動作」の結果としてあり得る動作の一つです。ちなみに、マルチスレッド分野でC++言語に大きな影響を与えたJava言語では、もう少し穏やかな定義「データ競合が生じると何らかの状態になる(が異常停止はしない)」となっています。

*4:組込み型やプリミティブ型と呼ばれることもあります。なお、C++言語仕様での正式名称はスカラ型(scalar type)です。

*5:id:y-hamigakiさんのHamigaki C++ライブラリでは、SGI STLより明快にスレッドセーフ保証について記載しています。

volatile教、あるいはvolatile狂

かつてのMicrosoft Visual Studio .NET 2003のC/C++コンパイラ(MSVC7.1)には、「volatile変数にオレオレ定義の意味を与えて最適化を行う」というアグレッシブすぎるオプションが存在したという昔話。

どんなもの?

このオプションでは、volatileキーワードにC/C++標準規格とは全く異なる独自の意味を与えて、コード生成時の最適化処理に利用します。つまり、コンパイル時に下記を前提としたコード生成を行うのです。

  • 通常の変数=変数実体にアクセスする手段は1通りだけ(aliasingが無い)
  • volatile変数=上記制約の範囲外(aliasingが有る)

これは「volatile=aliasingが有りえる」というMSVC7.1だけの独自拡張です。*1

このaliasing(エイリアシング; 別名)という単語、少々耳慣れないかもしれません。aliasingが無いとは、“ある変数が指す実体にアクセス”するとき、その実体への“別の変数経由でのアクセス”が存在しないことを意味します。具体例を挙げると、int a = 0;に対してint *p = &a;を定義したとき、a*pは“同一の実体(メモリ)”を指します。その後の処理でa = 42;int b = *p;のように異なる変数を介してアクセスするとき、この実体に対してはaliasingが有ると言えます。*2

実はこのaliasingの有無は、コンパイラの最適化処理にとって非常に厄介なものです。通常はaliasingが有ると仮定を置いたコード生成を行わざるをえず、積極的な最適化ができないというケースが多々あります。*3

具体的には何をするの?

この最適化オプションの働きについて、マニュアル(MSDN)を引いてみましょう。

/Oa または /Ow を使用する場合は、次の規則に従う必要があります。次の規則は、volatileとして宣言されていないすべての変数に適用されます。

  • 直接使われている変数をポインタで参照しません。変数が代入式の左辺または右辺で使われているか、あるいは関数の引数として使われていると、その変数が参照されます。
  • 変数へのポインタが使われている場合は、その変数を直接使用できません。
  • 変数のアドレスを関数で取る場合は、その変数を直接使用できません。
  • ポインタによってメモリ位置が変更された場合は、別のポインタでそのメモリ位置にアクセスできません。

上記の規則に従わないと、データが破損する可能性があります。

/Oa、/Ow (エイリアスを使わないと仮定、関数呼び出しでエイリアスを使うと仮定)

…上記のMSDN日本語訳では意味が取りづらい気がしますので、素直に英語版MSDNを参照すべきかもしれません。

If you use /Oa or /Ow, you must follow these rules. The following rules apply for any variable not declared as volatile:

  • No pointer can reference a variable that is used directly (a variable is referenced if it is on either side of an assignment or if a function uses it in an argument).
  • No variable can be used directly if a pointer to the variable is being used.
  • No variable can be used directly if its address is taken within a function.
  • No pointer can be used to access a memory location if another pointer modifies the same memory location.

Failing to follow these rules can cause corrupted data.

/Oa, /Ow (Assume No Aliasing, Assume Aliasing Across Function Calls)

ちょっと何言ってるか分からない

MSDNに掲げられた4つのルールに従うと、下記ソースコードを/Oaオプションありでコンパイルした場合、実行時にデータが壊れて悲惨な結果が得られるのことです(おそらく、予期しない値を読み込んだように見えるでしょう)。いずれもC/C++言語ではごくありふれた処理ですし、このようなソースコードが正しく実行できないとなると、もはや普通のプログラムを書ける気がしません。

int x = 0;
int *p;
void rule1()
{
  x = 42;  // 直接xを使うとき...
  p = &x;  // NG: ポインタが参照してはダメ!
}
int x = 0, y;
int *p = &x;
void rule2()
{
  *p = 42;  // ポインタ経由で使うとき...
  y = x;    // NG: 直接xを使ってはダメ!
}
int x = 0;
void rule3()
{
  int *p = &x;  // 関数内でアドレスをとると...
  x = 42;       // NG: 直接xを使ってはダメ!
}
int x = 0, y;
int *p1 = &x;
int *p2 = &x;
void rule4()
{
  *p1 = 42;  // あるポインタ経由で使うと...
  y = *p2;   // NG: 別ポインタ経由で使ってはダメ!
}

volatile教のすゝめ

MSVC7.1曰く、/Oaオプションありでも正常動作させるならば、volatileキーワードを利用せよと。ここまで来ると、もはやvolatile教信者というよりvolatile狂信者の様相を呈しています。

volatile int x = 0;
volatile int *p;
void rule1()
{
  x = 42;  // 直接xを使うとき...
  p = &x;  // ポインタが参照してもOK!
}
volatile int x = 0, y;
volatile int *p = &x;
void rule2()
{
  *p = 42;  // ポインタ経由で使うとき...
  y = x;    // 直接xを使ってもOK!
}
volatile int x = 0;
void rule3()
{
  volatile int *p = &x;  // 関数内でアドレスをとると...
  x = 42;       // 直接xを使ってもOK!
}
volatile int x = 0, y;
volatile int *p1 = &x;
volatile int *p2 = &x;
void rule4()
{
  *p1 = 42;  // あるポインタ経由で使うと...
  y = *p2;   // 別ポインタ経由で使ってもOK!
}

Don't Try This At Home, I'm A Professional Volatileian.

その後…

結局のところ、実プロダクトでこの最適化オプションを活用できる場面が無かったのでしょう。次バージョンMicrosoft Visual Studio .NET 2005(MSVC8.0)ではあっさり削除されましたとさ。

  • /Oaコンパイラオプションが削除されましたが、エラーなしで無視されます。(後略)
  • /Owコンパイラオプションが削除されましたが、エラーなしで無視されます。(後略)
Visual Studio 2005 - コンパイラの新機能

R.I.P to /Oa, /Ow options.

*1:C/C++標準規格では(主として)「volatile=C/C++実行環境の“外”にリンクしている」を意味し、代表的な例ではメモリマップドI/Oなどでvolatile変数が利用されます。よく「volatile=最適化の抑止」のような表現がされますが、これは「volatile変数アクセス⇒C/C++言語仕様の“外”への干渉/未知の“何か”を操作する⇒コンパイラが直接知りえない操作は勝手に最適化できない」から帰結される一側面に過ぎません。

*2:本記事中では言及しませんが、C言語(C99以降)にはrestrictキーワードが導入されており、この最適化オプションが目指していた事をより安全に達成できます。つまり、プログラマ自身が「aliasingが無い」と分かっている箇所のみを明示し、普通の変数アクセスでは従来通り「aliasingが有る」という仮定が置かれます。詳細はrestrictキーワード続 restrictキーワードなどを参照ください。

*3:C/C++言語の文脈では“Strict Aliasing Rule”という規則があり、これにより一定の条件下ではaliasingが無いと仮定した積極的な最適化処理ができます。詳細は翻訳記事“C/C++のStrict Aliasingを理解する”あたりをどうぞ。

FizzBuzz化ストリーム

インターネット上で定期的に話題にのぼる「FizzBuzz問題」を、C++言語で変な風に解いてみたというお話。

今ではすっかり有名になったFizzBuzz問題ですが、元々はJeff Atwood氏による2007年の記事Why Can't Programmers.. Program?青木靖氏による日本語訳)が初出のようです。ルールをおさらいしておくと、次の仕様を満たすプログラムを作れという問題ですね。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

困ったときのおまじない

FizzBuzz問題はあまりにも有名なため、Web検索すればいくらでも実装コード例が見つかります。いまさら普通に解いてもしょうがない(?)ので、C++標準ライブラリI/Oストリームを利用して非侵襲的(nonintrusive)な解法をとりたいと思います。

つまり、オリジナルのこんなコードに対して:

#include <iostream>
const int N = 16;

// 1...Nまでの数字を出力
template <class OutputStream>
void countup(OutputStream& os)
{
  for (int i = 1; i <= N; ++i)
    os << i << os.widen(' ');
  os << std::endl;
}
 
int main()
{
  // countup()関数を呼ぶ
  countup(std::cout);
}
// 出力結果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

標準出力ストリームstd::coutに小細工をすると…

int main()
{
  fizzbuzznizer fb(std::cout);  // おまじない

  // 先ほどと同じcountup()関数を呼ぶだけ
  countup(std::cout);
}
// 出力結果:
// 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16

元の出力処理(countup関数)には一切手を加えずに(=非侵襲的)、ちゃんとFizzBuzz問題が解けました。

ちなみにこの“おまじない”、出力処理側のコードには依存しないので:

fizzbuzznizer fb(std::cout);  // おまじない

// 出力が文字列でもOK
std::cout << "seq={1 2 3 4 5 6...}";
  // 出力結果:seq={1 2 Fizz 4 Buzz Fizz...}

// Boost.Formatとの組み合わせもOK
#include <boost/format.hpp>
std::cout << boost::format("%1% and %2%, %3%") % 1 % 42 % 105;
  // 出力結果:1 and Fizz, FizzBuzz

また出力ストリーム(std::ostream派生クラス)を対象として、こんなことも:

#include <sstream>
std::ostringstream oss;
fizzbuzznizer fb(oss);  // おまじないをossにふりかける

// ostringstreamでもいけるよ
oss << 6 << "/10";
assert(oss.str() == "Fizz/Buzz");

C++万歳!I/Oストリーム万歳!

タネ明かし

先ほど使った怪しい“おまじない”は、下記コードで実現されます。
実装の概略:

  • basic_fizzbuzznizer:対象出力ストリームオブジェクトのストリームバッファを、下記basic_fizzbuzz_streambufに差し替える。
  • basic_fizzbuzz_streambuf:対象ストリームオブジェクトから渡されてくる文字に対してFizzBuzz化を行い、結果を元ストリームバッファに転送する。
  • ジェネリックな記述によりwchar_t等にも(無駄に)対応。


basic_fizzbuzznizerクラステンプレート

同オブジェクトの変数スコープにあわせて、対象出力ストリームオブジェクト(std::coutなど)のストリームバッファを置換/復元するヘルパクラスです。

通常は「std::cout → {coutのストリームバッファ}」のようになっている関連付けを、コンストラクタにて「std::cout → basic_fizzbuzz_streambuf → {coutのストリームバッファ}」のように差し替え、またデストラクタでは関連付けを元に戻しています。

basic_fizzbuzz_streambufクラステンプレート

実際にFizzBuzz化処理を行う、std::basic_streambufから派生するストリームバッファクラスです。basic_fizzbuzz_streambufは一般的なストリームバッファと異なり、ストリームクラスから渡されてくる文字を変換(FizzBuzz化)するだけで、本当の“出力”処理は元のストリームバッファへ委譲します。

さて、FizzBuzz問題における倍数判定は「数値」に対して行う必要がありますが、残念ながらストリームバッファで受け取れるデータは「文字」シーケンスだけです。これは前段のストリームオブジェクトで整形(format)された結果であり、I/Oストリームライブラリ設計に起因しています。

ただ幸いなことに必要な倍数判定は3と5のため、次の代替判定アルゴリズムを利用できます。また、与えられた「文字」シーケンスに対して、“数字で構成される部分シーケンス”を「数値」として解釈することにします。

  • 3の倍数=各桁の数値の合計値が3の倍数
  • 5の倍数=最下位桁が0または5

以上をもとに、basic_fizzbuzz_streambuf各メンバ関数では次の処理を行います。

コンストラクタ
元ストリームバッファを保持し(sb_)、自身の出力領域(put area)サイズを0に設定する(setp(0, 0))。これにより、1文字受け取るたびにoverflow()が呼び出されるようになる。
overflowオーバーライド
出力領域があふれる=ストリームバッファへ1文字入力された時に呼び出される。入力が“数字”であれば一時バッファ(tb_)で追加保持し、3の倍数判定準備(acc3_)と5の倍数判定(mul5_)を行う。数字以外の場合はそのまま元ストリームバッファへ転送する(sb_.sputc(c))。
syncオーバーライド
フラッシュ要求がなされた時に呼び出される。倍数判定結果に応じて出力文字列を決定し、putn()ヘルパ関数を経由して元ストリームバッファへ流し込む(sb_.sputc(...))。
fizzbuzznizer

仕上げに、C++標準ライブラリ風のchar/wchar_t用typedefを準備して完成です。

typedef basic_fizzbuzznizer<char> fizzbuzznizer;
typedef basic_fizzbuzznizer<wchar_t> wfizzbuzznizer;

本当は怖くないムーブセマンティクス

この記事はC++ Advent Calendar 2012の15日目にエントリしています。
内容はC++11「ムーブセマンティクス」の入門記事となっています。

f:id:yohhoy:20121208022541j:plain
もくじ

  1. ムーブセマンティクス再考
    1. シンタックス vs. セマンティクス
    2. コピー vs. ムーブ
    3. ムーブのもつ2つの意味
    4. C++11のムーブセマンティクス対応
  2. auto_ptrからunique_ptrへ
    1. auto_ptrの暗い過去
    2. unique_ptrへの移行
    3. std::moveの役割
  3. ムーブセマンティクスを使おう
    1. C++11標準ライブラリとムーブ
    2. ムーブ"後"の中身は?
    3. ムーブを利用して関数を書く

(本文のみ約9500字)

まえがき

To move or not to move: that is the question.
  ― Bjarne Stroustrup, 2010(改)*1

プログラミング言語C++の新しい国際標準規格*2、いわゆるC++11が2011年9月に正式発行されてから1年ちょっとが経ちました。このC++11の新機能として、よく「ムーブセマンティクス(move semantics)」と「右辺値参照(rvalue reference)」がセットで紹介されます。Web上でも多くの入門・解説記事が見つかりますが、この2つがどんなものか把握できたでしょうか?実際のところ、「ムーブセマンティクス」はそれほど複雑なモノではありません。ですが、新しい「右辺値参照」や以前からの「(左辺値)参照」などの見慣れない用語を使って説明されると、最初のハードルが高く感じられるかもしれません。少なくとも私自身はそうでした…

そこで本記事のアプローチは:右辺値参照」の解説を完全に省いてしまい、ムーブセマンティクス」の解説と、クラスライブラリ利用者視点でのムーブ利用方法のみ説明していこうと思います。

対象とするのは、C++標準ライブラリ提供の“ムーブセマンティクスに対応した既製クラスを利用した関数の記述”までとし、“自作クラスをムーブセマンティクスに対応させる”などは範囲外となります。後者ではどうしても「右辺値参照」の説明が必要になりますし、そもそも利用者の観点から「ムーブセマンティクス」を理解しないことには、クラスの提供者になれるはずがありません。

おことわり:本記事では、ムーブセマンティクスと強い関係にある「右辺値参照」の説明を省いたため、正確性に欠ける表現となっている箇所もあります。なるべく脚注にて補記したつもりですが、その点は予めご了承ください。また「右辺値参照」の詳細については、より高度な解説記事を探してみて下さい。



1. ムーブセマンティクス再考

「ムーブセマンティクス」は、“ムーブ”+“セマンティクス”という2つの単語からなる用語です。まずは各単語の意味について考え、そこから「ムーブセマンティクス」を説明します。続いて、C++11の新機能との関係を整理しましょう。

1.1. シンタックス vs. セマンティクス

「ムーブセマンティクス」の後半部分、セマンティクス(semantics; 意味論)とは何でしょう?これと対比される単語、シンタックス(syntax; 構文)は?プログラミング言語の世界では、それぞれ次のように定義されます。

シンタックス(syntax)
目的の動作をするプログラムを、ソースコードとして具体的にどのように記述するかというルール。
セマンティクス(semantics)
ソースコードとして表現されるプログラムが、概念的にどのような意味を持っているかというルール。

セマンティクスの方は抽象的で少々難解ですが、もっと単純化すると次のようになります。

プログラミング言語は、プログラムで実現したい機能(セマンティクス)をソースコード上に表現するために、どのようにソースコードを書けばいいのか(シンタックス)を定める、ルールの集合体であるとも解釈できます。*3

簡単な例を挙げてみましょう。C++言語では、“数値加算”や“変数への代入”というセマンティクスを表現するため、記号 += を用いたシンタックスが提供されます。

// 数値1と2を足し算する。その結果を変数tへ代入する。
t = 1 + 2;
// 変数tの数値を取り出し、変数uへ代入する。
u = t;

…あまりに単純過ぎて小石でも投げつけられそうですが、この2つを区別することは非常に重要です。先の例ではシンタックスとセマンティクスが1:1で対応していますが、実はそうではない例も存在します。C言語を祖先にもつC++言語では、配列の要素への添え字アクセスがそれです。

// 配列aから4番目の要素を取り出し、変数bへ代入する。
b = a[4];
// 同
b = *(a + 4);

ここでは同じセマンティクスに対して、2種類の異なるシンタックスが使えます。一般的には配列要素アクセスは前者で記述しますが、実はこれ、シンタックス・シュガー(syntax sugar; 糖衣構文)の一種です。本来、C言語での配列要素アクセスは後者 *(a + 4) で十分なのです。しかし、これではソースコードから意図をくみ取るのが困難なため(ですよね?)、前者 a[4] というシンタックスを別途用意し、“配列に添え字で要素アクセスする”というセマンティクスを読み取り易くしています。

まずは、シンタックスとセマンティクスの違いを説明しました。また両者の関係は必ずしも1:1でない、という点も押さえておいてください。

1.2. コピー vs. ムーブ

あとは「ムーブセマンティクス」の前半部分、ムーブ(move; 移動)についてです。ムーブと対比されるのはコピー(copy; 複製)です。まぁ、この2つの違いは丁寧に説明しなくてもイメージを掴めるでしょう。ここでは変数tから変数uへ、その中身(文字列)をコピー/ムーブするイメージイラスト1枚でお茶を濁します。
f:id:yohhoy:20121210215421p:plain

ところで、C++言語における変数は“値のセマンティクス(value semantics)”を持つと言われます。ムーブセマンティクスの説明も途中なのに、新しい類似語を出すなんてと怒られそうですが、お話はちゃんと繋がっていますから。

さて、この“値のセマンティクス”を一言で表すと、“int型と同様に振る舞うこと”といえます。例えば文字列型std::stringでは、代入によって値(文字列)がコピーされ、別々の変数の中身はそれぞれで保持されます。そのため変数s2の中身を書き換えても、別の変数s1の中身には影響がありません。さらに標準コンテナstd::vectorなど、その中身として複数要素を保持するような型であっても同じことが言えます。*4

int i1 = 1;
int i2 = 2;
i1 = i2;  // i1==i2==2
i2 = 42;  // i1==2 && i2==42

std::string s1 = "apple";
std::string s2 = "banana";
s1 = s2;  // s1==s2=="banana"
s2 = "";  // s1=="banana" && s2==""

std::vector<int> v1;
std::vector<int> v2 = {1, 2, 3, 4};
v1 = v2;      // v1==v2=={1, 2, 3, 4}
v2[0] = 100;  // v1=={1, 2, 3, 4} && v2=={100, 2, 3, 4}

何を今更こんな当然の事を…と思いました?それは“C++言語の変数は値のセマンティクスを持つ”という概念モデルを、すでに認識しているからと言えます。

改めて先程のシンタックス/セマンティクスの関係で説明すると、代入シンタックス u = t は、変数tの中身をuへコピーするというセマンティクスを持ちます。ちなみに、ご存知の通り、文字列やコンテナのコピー処理にはそれなりの実行時コストがかかります。コピー元+コピー先分で2倍のメモリ領域が必要となりますし、コンテナの場合はその全要素値について個々にコピーが必要となるからですね。

1.3. ムーブのもつ2つの意味

先の“値のセマンティクス”を実現するには、本質的にコピーが必要でした。しかし実際のC++プログラムでは、必ずしもコピーしなくても十分なケースがあります。

std::vector<int> twice_vector(std::vector<int>);  // 全要素値を2倍して返す関数

std::vector<int> v = { /*大量の要素*/ };
std::vector<int> w = twice_vector( v ); // 関数へ変数vの中身を渡したい
// これ以後は変数vを使わない

この変数vの中身は、関数twice_vectorへ渡した後はもう必要とされません。なのに、関数呼び出し部分では必ずコピーが発生しています。C/C++は実行時コスト最小化のためなら、自分の足を撃ち抜くリスクすら厭わないプログラミング言語のはず。こんなことで良いのでしょうか!?

まぁ、この程度の例だと“参照型(std::vector<int>&)使え”で一蹴されるでしょうが、ここはムーブの出番でもあります。実行時コストが高くつく変数の中身のコピーを避けて、変数の中身を移動(ムーブ)してしまえば実行時コストを抑えられる、まさにそういうケースです。つまり、ここではムーブを「最適化されたコピー」と考えています。仮にここをコピーのままとしても、実行時コストが無駄にかかるだけで、プログラムの動作としては何ら影響がありません。*5

逆に、移動(ムーブ)でないとプログラムの意味が変わって/壊れてしまうケースもあります。例えばBoost.Smart Pointersライブラリ提供のスマートポインタboost::unique_ptrでは、“あるオブジェクトを指すunique_ptr変数はただ1つ”と決められています。仮にunique_ptr変数をコピーしてしまうと、このクラスの決め事(不変条件)に違反するため、この手のクラスではコピーが禁止されています。一方でムーブならば、変数pの中身を変数qへ移し、それと同時に元の変数pは何も指さなくすることで、クラスの不変条件を守ることができます。ここではムーブを「所有権の移動」と考えています。

boost::unique_ptr<int> p( new int(42) );
boost::unique_ptr<int> q = p;  // コピー禁止!(コンパイルエラーになる)

boost::unique_ptr<int> q ← p;  // ムーブならOK('←'はムーブを表すと仮定)
assert(*q==42 && p.get()==NULL);

というわけで、移動(ムーブ)という操作がもつ2つの意味(セマンティクス)、すなわち「ムーブセマンティクス」の異なる側面「最適化されたコピー」「所有権の移動」を整理しました。以降しばらくは、本質的にムーブを必要とする後者を例にあげて説明を続けます。

1.4. C++11のムーブセマンティクス対応

さて、ここまでの説明から“「所有権の移動」なら普通のポインタ(T*)でも出来るんじゃない?”と疑問がわくかもしれません。

int *p = new int(42);  // ポインタpが値42を所有する
int *q;

// pからqへ所有権を移動する
q = p;
p = NULL;
assert(*q==42 && p==NULL);

はい。正解です!変数pからqへの所有権の移動は、ポインタのコピー+移動元のNULLクリアの組合せでもちゃんと実現できます。つまり「ムーブセマンティクス」という考え方そのものは、以前からある考え方であり、かつ実装パターン(イディオム)に過ぎないのです。では、なぜC++11の新機能として「ムーブセマンティクス」が紹介されるのでしょうか。

先ほどの“普通のポインタによる所有権の移動”にはちょっとした問題があります。1回のムーブを表現するのに、2つの独立した操作(代入2回)を必要とします。このため、プログラマ自身がポインタ操作に十分気を配らないと、簡単にムーブセマンティクスが破壊されてしまいます。結局のところ、このように実現するムーブセマンティクスは、プログラマが勝手に決めたローカルルールでしかありません。

// pからqへ所有権を移動する?
q = p;
// "p = NULL;"を忘れると... 意図せず pからqへのコピー になる

ここで仮の話として、ムーブセマンティクスを直接表現する架空のシンタックス y ← x を導入してみましょう。もしこのシンタックスがあれば、所有権の移動を表すコードはシンプルで意図が明確になり、ポインタ版のような人為ミスによるセマンティクス破壊も起きえません。

SmartPtr p( new int(42) );  // ポインタpが値42を所有する
SmartPtr q;

// pからqへ所有権を移動
q ← p;

この“←演算子”は架空の話でしたが、実際のC++11でも「ムーブセマンティクス」を(間接的に)表現する、新しいシンタックスが追加されました。この新しいシンタックスこそが、C++11の「右辺値参照」として紹介される言語機能なのです。シンタックス追加によってプログラミング言語の仕様を拡張することで、コンパイラからムーブセマンティクスを認識できるようになり、これによりコンパイル時の最適化といった場面で有利にはたらきます。(どちらかと言えば暗黙のムーブ対応において意義があると思うのですが、これは本記事の範囲を超えます。)

それでは、C++11での「ムーブセマンティクス」と「右辺値参照」の関係を整理しましょう。

  • 「ムーブセマンティクス」は以前からある、単なる実装パターン(イディオム)です。
  • C++11ではコンパイラから「ムーブセマンティクス」を認識できるよう機能拡張します。
  • この目的のために追加された新機能が「右辺値参照」というシンタックスです。



2. auto_ptrからunique_ptrへ

ここからは、C++標準ライブラリ提供のスマートポインタstd::auto_ptrstd::unique_ptrを用いて、直接的にC++11の「ムーブセマンティクス」を表現する、具体的なシンタックスを見ていきましょう。先ほどは、「右辺値参照」で“間接的に”ムーブセマンティクスを表現するとしましたが、ここで初めて std::move を使ったシンタックスが登場します。
(注:これ以後登場する"C++03"は、C++11発行前の国際標準規格であった過去のC++言語を指します。…現実には当面の間C++03が使われ続けるでしょうけど。)

2.1. auto_ptrの暗い過去

古き良きC++03標準ライブラリのなかに、1つだけ「ムーブセマンティクス」をサポートしたクラスがありました。それがstd::auto_ptrです。

std::auto_ptr<int> p( int new(42) );  // ポインタpが値42を所有する
std::auto_ptr<int> q;

// pからqへ所有権を移動(ムーブ)
q = p;
assert(*q==42 && p.get()==NULL);

あまり積極的に利用されてこなかった気もしますし、std::auto_ptrは標準コンテナに格納できないといった問題もあって、C++11標準ライブラリではあえなく非推奨(deprecated)となりました。

さて、ここまでのシンタックス/セマンティクスの説明を踏まえ、もう一度std::auto_ptrを使ったコードをみると、何かおかしな事に気づきませんか?

std::string s1 = "apple";
std::string s2;

s2 = s1;  // s1からs2へ"コピー"
// s1==s2=="apple"

std::auto_ptr<int> p1( int new(42) );
std::auto_ptr<int> p2;

p2 = p1;  // p1からp2へ"ムーブ"
// p1.get()==NULL && *p2==42

どちらも同じ代入シンタックス u = t にも関わらず、“値のセマンティクス”を持つ普通の型std::stringではコピーが行われるのに対して、std::auto_ptrではムーブが行われています。もちろん、この動作そのものはstd::auto_ptrクラスの仕様通りなのですが、結果として同じシンタックスを用いているのに、std::auto_ptrだけが異なるセマンティクスを持つという状態になっています。

2.2. unique_ptrへの移行

C++言語の変数では“値のセマンティクス”が基本となるため、代入シンタックス u = t は値のコピーというセマンティクスを持つべきですし、C++プログラマにもこれが共通認識としてあるはずです。結局のところ、std::auto_ptrクラスが非推奨となった原因は、シンタックスから期待されるセマンティクスとの不一致、それによる使いづらさ・間違いやすさから来ています。

引退したstd::auto_ptrの代わりに、C++11からはstd::unique_ptrが追加されます。セマンティクスの観点からみると、両クラスともコピーを許容せず、所有権の移動のみが可能なスマートポインタという意味では同一です。この2クラスの差異は、ムーブセマンティクスを実現するシンタックスの観点で明らかになります。*6

// C++03時代 auto_ptr
std::auto_ptr<int> p1( int new(42) );
std::auto_ptr<int> p2;
p2 = p1;  // 代入シンタックスを用いて"ムーブ"を表す(混乱のもと)

// C++11以降 unique_ptr
std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;
p2 = p1;  // 代入シンタックスは"コピー"を表す;コピー不可のためコンパイルエラー

std::unique_ptrで普通に代入シンタックス u = t を用いると、セマンティクスとしては所有権の複製(コピー)を表します。そしてこのスマートポインタではコピーが禁止されているため、この書き方はコンパイルエラーとなるのです。もう一度std::auto_ptrと比較してみてください。

2.3. std::moveの役割

ここで初めて、「ムーブセマンティクス」を直接表現するシンタックスが必要となります。C++11では代入演算子 =std::move を組み合わせたものがムーブ代入シンタックス u = std::move(t) となり、これによってムーブセマンティクスを表現します。*7

std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;

// p1からp2へコピー
p2 = p1;             // (コピー)代入シンタックス
// コピー不可のため上記はコンパイルエラー!

// p1からp2へ所有権を移動
p2 = std::move(p1);  // ムーブ代入シンタックス
// p1.get()==NULL && *p2==42

コピー/ムーブの区別を明確にするため、これまで単に代入シンタックスと呼んでいたものは、コピー代入シンタックス u = t としておきましょう。

一旦シンタックスの話を整理するため、まずは昔ながらのコピーから考えます。C++言語で値の複製(コピー)というセマンティクスに利用するのは、「コピーコンストラクタ(copy constructor)」と「コピー代入演算子(copy assignment operator)」シンタックスの2種類です。(ここまでは代入演算子のみ取り上げていました)

T t1, t2, u2;  // Tという適当な型を仮定

T u1( t1 );  // コピーコンストラクタによる"コピー"
u2 = t2;     // コピー代入演算子による"コピー"

対応するムーブ版は、それぞれ「ムーブコンストラクタ(move constructor)」と「ムーブ代入演算子(move assignment operator)」シンタックスとなります。どちらも std::move が間にはさまること以外は、コピー版と同じ構造をしています。*8

T u1( std::move(t1) );  // ムーブコンストラクタによる"ムーブ"
u2 = std::move(t2);     // ムーブ代入演算子による"ムーブ"

最後に、コピー/ムーブに対応するシンタックスを下表にまとめます。ね、簡単でしょ?

コピー ムーブ
コンストラクタ T u( t ) T u( std::move(t) )
代入演算子 u = t u = std::move(t)


わき道小話1
ところで std::move の正体は何でしょうか?実はコレ、C++標準ライブラリ提供の単なる関数テンプレートです。標準ヘッダ<utility>で提供されるため、前掲サンプルコード群では正確には同ヘッダをincludeする必要がありました。

#include <utility>

本記事の説明範囲では、“コンストラクタ/代入演算子std::move組み合わせによってムーブセマンティクスを表現する”より詳細化できないため、これ以上は深追いしません。1つだけ注記するなら、std::move関数を単体で使っても“何も起こらない”という事だけ覚えておいてください。

T t, u;
std::move(t);      // 何も起こらない...
u = std::move(t);  // tからuへムーブ



3. ムーブセマンティクスを使おう

続いて、ムーブセマンティクスに対応したクラスライブラリの利用方法、つまりライブラリ提供のある型(クラス)がムーブセマンティクスに対応するとき、そのクラスの使い方ついて説明していきます。ここではC++11標準ライブラリを例に取りあげます。なんと言っても“標準”ですから。

3.1. C++11標準ライブラリとムーブ

C++標準ライブラリは、文字列/標準コンテナ/スマートポインタなどのプログラム基礎部品として使えるクラス群を提供します(正確には“クラステンプレート”で提供されますが、簡単のためクラスで説明を続けます)。C++11標準ライブラリで提供されるクラス群は、基本的に全てムーブセマンティクスに対応しました。これはC++11で新規追加されたクラスstd::unique_ptrだけでなく、C++03時代から存在する馴染みのクラスstd::stringstd::vectorなども、C++11ではムーブセマンティクスに対応したという意味です。

std::string s1 = "apple";
std::string s2 = "banana";

s1 = std::move(s2);  // s2からs1へムーブ
// s1=="banana"

std::vector<int> v1;
std::vector<int> v2 = {1, 2, 3, 4};

v1 = std::move(v2);  // v2からv1へムーブ
// v1=={1, 2, 3, 4}

std::stringstd::vectorは“値のセマンティクス”をもつため、もちろん従来通りコピーすることもできます。言い換えると、これらのクラスではムーブセマンティクスの導入によって、ムーブを「最適化されたコピー」として利用できるようになりました。またコンテナクラスでは、“コンテナ全体のムーブ”と“コンテナ要素に対するムーブ”の両方が拡張されています。

std::string s1 = "apple";
std::string s2 = "banana";

std::vector<std::string> dict;

dict.push_back( s1 );             // s1からコンテナ内へ"コピー"して要素追加
// s1 == "apple"
dict.push_back( std::move(s2) );  // s2からコンテナ内へ"ムーブ"して要素追加
// (s2の状態は後述)

std::vector<std::string> v;
v = std::move(dict);  // コンテナ全体をdictからvへムーブ
// v[0]=="apple" && v[1]=="banana"

また、既に説明したとおりstd::unique_ptrはムーブを「所有権の移動」に利用します。さらに、C++03時代は単にコピーが禁止されていたI/Oストリームクラスstd::fstream, std::stringstreamなどでは、C++11以降はムーブによる「所有権の移動」が出来るようになりました。

とはいえ、C++標準ライブラリのあらゆるクラスでムーブ可能となった訳ではありません。中にはC++11で追加されたスレッド間排他制御ミューテックスstd::mutexのように、コピーもムーブも両方禁止というクラスも存在します。

まとめると、C++11標準ライブラリ提供のクラスは3種類に分類されます。

  • コピー・ムーブ両方が可能なクラス(ムーブは「最適化されたコピー」)
  • コピー禁止だがムーブのみ可能なクラス(ムーブは「所有権の移動」)
  • コピー・ムーブ両方が禁止されるクラス

わき道小話2
C++標準ライブラリのうち“値のセマンティクス”をもつクラスでは、「最適化されたコピー」としてのムーブに対応したと説明しました。ところが、クラスの内部実装によってはこれ以上最適化できない、というケースも存在します。このようなクラスはコピー/ムーブともに可能ではあるものの、コピー/ムーブで同程度の実行時コストが掛かってしまいます。

#include <array>
std::array<int, 100> a1 = { /*...*/ };
std::array<int, 100> a2 = { /*...*/ };

a1 = a2;             // a2からa1へ"コピー"
a1 = std::move(a2);  // a2からa1へ"ムーブ"? →実際は"コピー"

この手のクラスでは、コピーコンストラクタ/代入演算子だけが提供され、ムーブコンストラクタ/代入演算子は提供されません。ただ、この場合にもムーブ代入シンタックス u = std::move(t) はちゃんとコンパイルできます。しかし実際にはコピー代入演算子が呼びだされ、結果としてコピーの処理が行われるという動きになります(詳細は本記事の範囲を超えます)。C++11標準ライブラリでは、少なくとも固定長配列クラスstd::array複素数クラスstd::complexがこのような対応を取っています。

3.2. ムーブ"後"の中身は?

さて、ここまでの説明で敢えて避けてきた話題があります。ムーブされた"後"の変数には何が入っているのでしょう?それらしい答えとしては“空っぽ”になる?

std::string t = "xmas", u;

u = std::move(t);  // tからuへムーブ
// u=="xmas" …ではtの中身は?

ええと、この質問に答えるのは少々厄介です。C++11標準ライブラリでは、ムーブ後は基本的に“有効だが規定されない状態(valid but unspecified state)”になると定めています。何を言っているのか意味不明ですが、この段階では一言も“ムーブ後は空っぽになる”と保証していないことに注意してください。

例えばstd::stringでは、まさにこの“有効だが規定されない状態(valid but unspecified state)”になります。簡単な表現では「中に何がはいっているか知りません(未規定)」だけど「変数への操作は出来るよ(有効)」という状態のことです。

std::string t = "xmas", u;
u = std::move(t);  // tからuへムーブ

// OK: t = "X";  再代入は問題無くできる
// OK: t.size()  サイズ問合せはできる …がどんな値かは不明
// NG: t[2];     中身がどうなっているか分からないからダメ(空っぽかもしれない)

結局のところムーブ後の中身がどうなるか規定されないケースでは、事実上“その変数への再代入”しか使い道がないと思います。

ただし例外事項として、ムーブが「所有権の移動」を表すクラスでは“ムーブ後の変数が確かに所有権を持たない”必要があり、この場合はムーブ後の変数が空っぽであると保証されます。(詳細はこのあたりを参考にしてください。)

std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;

p2 = std::move(p1);  // p1からp2へ所有権を移動(ムーブ)
// p1は所有権を持たない p1.get()==NULL ことを保証

ムーブ後の状態について一通り説明しましたが、普通は気にかける必要はありません。だって、その“変数”が不要になるからムーブで中身を取出すんでしょ?

3.3. ムーブを利用して関数を書く

最後に、自作の関数でのムーブセマンティクス利用方法について説明します(ここでは関数テンプレートではなく、普通の関数を対象とします*9)。ここまでは隣同士にある変数間でムーブしていましたが、実際のプログラムで利用する場面はまず無いでしょう。ムーブセマンティクスが本当に役立つのは、関数呼び出しをまたいで値をやり取りするケース、つまり (1)関数呼び出し時に引数へムーブで渡す、そして (2)関数から戻り値をムーブで返す の2パターンです。

いきなり答えを書いてしまうと、(1)と(2)両方でいわゆる値渡し(pass-by-value)」スタイルを使うのが正解となります。関数twice_vectorの (1)引数の型 と (2)戻り値の型 が、それぞれ普通のIntVecであることに注意して下さい。また (1)関数の呼び出し側 と (2)関数からのreturn文 の両方で std::move を使っていることにも着目してください。*10

// 全要素値を2倍して返す関数
typedef std::vector<int> IntVec;
IntVec twice_vector(IntVec a)
{
  for (auto& e : a)
    e *= 2;
  return std::move(a);  // (2)"ムーブ"で戻り値を返す
}

IntVec v = { /*...*/ };
IntVec w = twice_vector( std::move(v) );  // (1)"ムーブ"で引数へ値を渡す

C++03時代だと、この書き方は“コンテナ全体をコピーするなんて非効率極まりない”と四方八方からマサカリが飛んできますが、ムーブセマンティクス対応のC++11以降ではこれがベストになります。ちなみに、上記コードでstd::moveを消してしまえば、C++11であってもC++03時代と同じくコピーが行われます。
(引数の型に関する議論はここで簡単に比較しています。)

One more thing...

冒頭で「右辺値参照」については一切説明しないと言いましたが、ありがちな誤用例だけはアンチパターンとして紹介しておきたいと思います。
注意:右辺値参照型の使いどころを誤ると罠にハマるというお話です。関数の引数/戻り値では右辺値参照型を使えないという意味ではありません。

パターン1: 引数の型に右辺値参照型IntVec&&を誤用する。
関数呼び出し側ではコピーによる値渡しができなくなります。言い換えると、普通の“変数を引数として関数に渡す呼び出し”がコンパイルエラーになります。不便な関数仕様の出来上がり。

// 誤用パターン1
typedef std::vector<int> IntVec;
IntVec twice_vector(IntVec && a)  // 引数型 = IntVec&&
{
  for (auto& e : a)
    e *= 2;
  return std::move(a);
}

IntVec v1, v2, w1, w2;
w1 = twice_vector( v1 );             // NG: コピーのシンタックスで呼び出せない!
w2 = twice_vector( std::move(v2) );  // OK: ムーブのシンタックスのみ受け付ける

パターン2: 戻り値の型に右辺値参照型IntVec&&を誤用する。
ダメ!絶対!参照先の変数aが有効なのは、関数twice_vectorの中だけです。この変数への“参照”を返してしまうと、関数呼び出し側に戻ったときには既に参照先が存在しません。右辺値参照型もしょせんは“参照型”なのです。その実行結果は未定義動作となり、恐らくクラッシュなどを引き起こすでしょう。

// 誤用パターン2
typedef std::vector<int> IntVec;
IntVec && twice_vector(IntVec a)  // 戻り値型 = IntVec&&
{
  for (auto& e : a)
    e *= 2;
  return std::move(a);
  // ここで変数aの生存期間は終わり!
}

IntVec v1, v2, w1, w2;
w1 = twice_vector( v1 );             // NG: 未定義動作!
w2 = twice_vector( std::move(v2) );  // NG: 未定義動作!

既に見てきた通り、「右辺値参照」で「ムーブセマンティクス」を実現するという表現に嘘はありません。でもムーブを“利用する”だけなら、右辺値参照型を直接使う必要はないんです。くれぐれも誤用には気をつけて!
(本当に右辺値参照型が必要になるのは、ムーブに対応したクラスの自作や、関数テンプレートのテンプレート引数型で使うケースです。)



おわりに

以上で説明はおしまいです。おつかれさまでした。改めて要点を列挙しましょう:

  • 「ムーブセマンティクス」は昔からあるイディオムですが、C++11からはコンパイラもムーブセマンティクスを認識できるようになりました。
  • C++11標準ライブラリ提供のクラスは、「最適化されたコピー」または「所有権の移動」としてのムーブに対応しました。
  • コピー代入シンタックス u = t とムーブ代入シンタックス u = std::move(t) を区別し、用途に応じて使い分けましょう。
  • ムーブを利用するだけなら、「右辺値参照」を気にする必要はありません。関数の引数/戻り値では「値渡し」スタイルを使いましょう。

さて今回の「右辺値参照」なしで「ムーブセマンティクス」を説明する試み、いかがだったでしょうか?本記事が少しでも理解の助けになればと願います。

C++ Advent Calendar 2012 は 16日目 @fadis_ さんの記事へと続き(move to)ます。Happy Holidays!
f:id:yohhoy:20121208020855j:plain
flickr / scpgt

*1:ISO/IEC JTC1/SC22/WG21 N3174 paperより。

*2:正式名称は ISO/IEC 14882:2011 Information technology -- Programming languages -- C++ です。ちなみに、一つ前の国際標準だった ISO/IEC 14882:2003 のステータスはWithdrawn、つまり新しい標準規格C++11の改訂によって以前の標準規格C++03は取り下げられています。

*3:実際に、“標準化”されたプログラミング言語仕様書はこういった定義を行います。一方で、“コンパイラインタプリタなどの処理系実装ありき”で仕様化(?)されるプログラミング言語も存在します。ただし後者では別の処理系を実装するのが非常に困難となるため、言語仕様とその実装は区別して取り扱うべきだと思います。

*4:話を単純化するため、ここではポインタ型(T*)や参照型(T&, T&&)は無視します。C++言語の参照型は“値のセマンティクス”とは異なる“参照のセマンティクス(reference semantics)”を持ちます。一方で、ポインタ型はさまざまなセマンティクスをあわせ持っています。例えば、ポインタ型変数+さす先の値をセットで考えれば“参照のセマンティクス”を持つといえます。ただ、ポインタ型変数の中身(≒アドレス値)だけに着目すると“値のセマンティクス”とも言えます。本記事の文脈では、後者“値のセマンティクス”として扱います(後半のスマートポインタ例示が相当)。

*5:“最適化されたコピーとしてのムーブ”という考え方は、書籍Effective C++シリーズで有名なScott Meyers氏による記事Initial Thoughts on Effective C++11 "For Copyable Types, View Move as an Optimization of Copy"からの受け売りです。抄訳はこちら

*6:ここでの議論とは無関係なため、std::unique_ptrのカスタムデリータといった追加機能は無視します。

*7:ここでは簡単のため右辺値としてxvalueのみを想定し、prvalueからのムーブについては無視しています。

*8:本記事の範囲では std::moveありき で説明してしまいます。実際のシンタックス上では、コンストラクタ/代入演算子の引数の型が 左辺値参照型(T&)または右辺値参照型(T&&)のいずれか によってコピー/ムーブが識別されます(cv修飾は除去して考える)。

*9:関数テンプレートの引数では型推論が行われるため、「右辺値参照」なしではロクな説明ができません。よって割愛。

*10:実は今回の例では、(2)return文ではstd::moveを使わない方がベターです。C++コンパイラが正しく「暗黙のムーブ」対応していれば自動的にムーブされますし、さらに言うと通称“名前付き戻り値最適化(NRVO)”と呼ばれる仕組みによって、ムーブ処理さえ最適化(省略)できる可能性があります。本記事では「暗黙のムーブ」に言及しないため、常にstd::moveを明示指定することとします。