C++標準規格の修正提案 P0135R0 (2015-09-27) の意訳です。この提案に従って P0135R1 (2016-06-20) で C++17 原稿が修正されました (C++1z N4606)。
これにより RVO (戻り値最適化) が必須化されたようです。規格の体裁としては「そもそも最適化するも何も、コピーなんて初めからそこにはないよ」というしれっとした感じになるようです。
P0135R0 (原文): Guaranteed copy elision through simplified value categories
P0135R1 (修正): Wording for guaranteed copy elision through simplified value categories
以降訳文です。
『値分類を単純化してコピー省略を保証する』
ISO/IEC JTC1 SC22 WG21
P0135R0
Richard Smith
2015-09-27
この論文では、ある種類のコピー省略 (特に、コピー元が一時オブジェクトの場合) が起こることを保証し、さらに、その時コピー・ムーヴコンストラクタの意味論チェックをしないようにする規格変更を1つ提案する。今回の方針は、後付けでコピー省略を強制するのではなく、値分類 (value categories) の定義を弄ることによってそもそも無用なコピーが存在しえないようにする。
コピーの省略 (Copy elision)
ISO C++ では様々な場合にコピーを省略しても良い:
- 一時オブジェクトが他のオブジェクトを初期化するのに使われる場合。関数によって返されるオブジェクトや、throw-expression で作られる例外オブジェクトも含む[訳註: RVO]。
- スコープが切れる直前の変数が
return
またはthrow
される場合[訳註: NRVO]。 - 例外オブジェクトが変数に
catch
される場合。
コピーが実際に省略されるかどうかは処理系の気まぐれである。現実の処理系では上記第一項の場合にコピーを省略するのが実態だが、プログラムはこの振る舞いに依存してはいけない。コピー・ムーヴ操作を、それが実際に実行されないと分かっていたとしても、利用可能にしておかなければならない。この論文では上記第一項の場合についてだけ考える。もちろん NRVO (名前付き戻り値最適化・第二項) はパフォーマンスでユーザを満足させるために重要な機能だが、NRVO が可能かどうかの判定は微妙な問題を含むので実施を保証するのは難しい。
コピー省略を強制するべき理由
最近[訳註: 2015-09-27 当時]、std-proposals
メーリングリストのあるスレッドで、現在のコピー省略の戦略がいかに問題であるかの長いリストが掲載された。ここにその主立ったものを挙げる:
- プログラマとコンパイラの間で合意があったとしても、実際には呼ばれないコピー・ムーヴコンストラクタが要求される。これは、ムーヴ不能な型を返すファクトリー関数 (いわば名前付きコンストラクタ) を作るのが不可能かとても難しいということを意味する。この所為で、プログラマは動的メモリ確保かなにかを使ってこれを回避することを余儀なくされる。
struct NonMoveable { /* ... */ };
NonMoveable make() { /* コピーなしにこれをうまくやる方法は? */ }
- コピー省略とムーヴ構築のパフォーマンスの差は必ずしも無視できない。特に大きなオブジェクトの場合がそうだ。しかし現状では、可搬なコードはコピー省略が起こると想定できない。結果として一部のプログラマは依然として、戻り値ではなく結果用の仮引数 (out-parameters) を好んで使う。
- 「殆どいたるところ
auto
教」は、それが「殆ど」に過ぎないという大問題を抱えている[訳註: 「全て」の箇所で auto を使いたいが使えないケースがあるということ]。ムーヴ不能な型の変数を、同じ型の式で初期化できないからである。
auto x = make(); // ダメ。欲しくないムーヴが必要とされる。
// コンパイラがムーヴを実際に実行しない場合でもダメ。
- 処理系に依存するようなプログラムの実行時意味論を可能にしていては、可搬性やコード検証に難が残る。
強制コピー省略の雰囲気
struct NonMoveable {
NonMoveable(int);
NonMoveable(NonMoveable&) = delete;
void NonMoveable(NonMoveable&) = delete;
std::array<int, 1024> arr;
};
NonMoveable make() {
return NonMoveable(42); // OK. 返されるオブジェクトを直接構築する。
}
auto nm = make(); // OK. 'nm' を直接構築する。
値分類 (Value categories)
コピー省略を保証するためにここで取る手法は、C++ の glvalue と prvalue の定義 (直感に反してこれらは値の分類ではなくて式の分類である) を弄ることである。C++ は現在のところ値分類を以下のように定めている:
-
lvalue (いわゆる左辺値・歴史的に代入式の左辺に現れることができたため) は関数またはオブジェクトを示す。[ 例:
E
がポインタ型の式の場合、*E
はE
が指し示す先のオブジェクトまたは関数を参照する lvalue 式である。また別の例として、戻り値の型が左辺値参照の関数呼び出し結果は lvalue である。 - 例終わり ] - xvalue (消滅寸前値・"eXpiring" value) は (大抵) 寿命寸前のオブジェクトを参照する。従って、例えばそのリソースはムーブ可能である。右辺値参照に関連するある種の式 (8.3.2) が xvalue を生成する。[ 例: 戻り値の型がオブジェクト型への右辺値参照である関数の呼び出し結果は xvalue である (5.2.2)。 - 例終わり ]
- glvalue (一般化左辺値・"generalized" lvalue) は lvalue 及び xvalue である。
- rvalue (いわゆる右辺値・rvalue は歴史的に代入式の右辺に現れたため) は xvalue・一時オブジェクト (12.2) またはその部分オブジェクト (subobject)・またはオブジェクトに結び付けられていない値である。
-
prvalue (純粋右辺値・"pure" rvalue) は xvalue でない rvalue である。[ 例: 戻り値の型が参照でない関数の呼び出し結果は prvalue である。The result of calling a function whose return type is not a reference is a prvalue.
12
,7.3e5
, やtrue
のようなリテラルの値も prvalue である。 - 例終わり ]
しかし、これらの規則を理解し血肉とするのは大変だしややこしい。例えば、一時オブジェクトを作る式はオブジェクトであるが、それでは何故 lvalue ではないのか? 何故 NonMoveable().arr
は prvalue ではなく xvalue なのか? この論文ではこれらの規則をその意図が明確になるように言い換える。特に、glvalue と prvalue について次のような定義を提案する:
- glvalue の式は、その評価によってオブジェクト・ビットフィールド・または関数の位置の計算を起こす。
- prvalue の式は、それが現れる文脈でオブジェクト・ビットフィールド・または被演算子 (operand) の初期化を引き起こす。
つまり prvalue は 初期化を実行し、glvalue は位置を計算する ということである。
形式的に明示するなら以下のようになる。
glvalue :: Environment -> (Environment, Location)
prvalue :: (Environment, Location) -> Environment
ここまでで C++ に対する機能的変更はない。つまり、既存の式の分類は何も変わらない。しかし、何故式がそのように分類されるのかということが分かりやすくなる。
struct X { int n; };
extern X x;
X{4}; // prvalue: X のオブジェクトの初期化を表す。
x.n; // glvalue: x のメンバ n の場所を表す。
X{4}.n; // glvalue: X{4} のメンバ n の場所を表す。
// このメンバは消失寸前 (expiring) なので特に xvalue である。
using T = X[2];
T{{5}, {6}}; // prvalue: 2 個の X オブジェクトの初期化を表す。
T{{5}, {6}}[0]; // xvalue: 消失寸前の配列要素の場所を表す。
値分類の改善が意味すること
さて、値分類の簡明な説明を得たので、改めてこれらの分類の式がどう振る舞うべきか考えよう。特に、A
をクラス型とするとき、式 A()
は一時オブジェクトを作成すると現在規定されている。しかしこれは不要だ。何故なら prvalue は初期化を引き起こすのが目的であって、一時オブジェクトの作成は A()
の知ったことではないのだ。一時オブジェクトの作成は、それが現れた文脈が必要に応じて行うべきである。しかし多くの文脈でその必要はない。例えば:
// make() は prvalue (値で返す (by value)). 従って、これは
// NonMoveable 型のオブジェクトの初期化の例になっている。
NonMoveable make() {
// 'make()' で初期化されるオブジェクトは以下の
// コンストラクタ呼び出しで初期化される。
return NonMoveable(42);
}
// 'nm' を直接初期化するために 'make()' を使う。一時オブジェクトは作られない。
auto nm = make();
NonMoveable x = {5}; // 現状 OK.
NonMoveable x = 5; // NonMoveable x = NonMoveable(5) と等価なので、現状では
// ill-formed である (一時オブジェクトが作られるがムーヴできない)。
// この提案により OK. そもそも一時オブジェクトは作られない。
結論として、クラス型または配列型の prvalue 式は一時オブジェクトを作らない。代わりに一時オブジェクトは式が現れた箇所で必要に応じて作成される。一時オブジェクトの作成 (物質化・materialization) を要求する文脈は以下の通りである:
- prvalue が参照に束縛されるとき
- クラス型 prvalue に対しメンバアクセスが実行されるとき
- 配列添字アクセスが配列型 prvalue に対して実行されるとき
- 配列型 prvalue がポインタに decay するとき
- クラス型 prvalue に対しアップキャストが実行されるとき
- prvalue が discarded-value expression として使われるとき
一番最初の規則は、非クラス・非配列型の prvalue をサポートするために、既に標準に含まれる。違いは、この提案により、この言語規則がクラス型と非クラス型で同じになることである。
その他の可能性
この提案には他に2つの代替手法が考えられる。
-
何も変更しない。 今まで現在の規則でずっとやって来たので、このまま続ける。つまり、
- 一時オブジェクト周りの効率について保証はしない
- 実際に呼び出されない場合でもコピー・ムーヴコンストラクタを提供する必要がある
- ムーヴ不能な型についてファクトリー関数を書く良い手段は存在しない
- 他の方法でコピー省略を保証する。 具体的にどうするかは明らかではない。また、値分類を分かりやすく、そして学びやすくする機会を逸してしまう。
これらの代替手段が提案に勝る点は、Type(...)
が一時オブジェクトを生成するという概念モデルを維持するということである。しかし、このモデルは半分幻だ。実は Type
がクラス型のときだけの話である。
謝辞
著者[訳註: Richard Smith] は、この提案に対するフィードバックに関して David Krauss と Jonathan Coewishes に、そして様々なアイディアに関して std-proposals
での議論に参加した全ての人に謝意を表明したい。
以上訳文でした。