yohhoyの日記(別館)

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

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

この記事は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を明示指定することとします。