C++23から、左辺値参照を返す関数においてローカル変数を直接返すケースがコンパイルエラーとなるようになります。
int& f() { int n = 10; return n; // ng } int main() { int& r = f(); }
これは意図された振る舞いであるとはいえ個別の提案によって導入されたものではなく、一見関係なさそうな別の提案の副作用として導入されました。それはP2266R3 Simpler implicit moveという提案で、これはreturn
文における暗黙ムーブ仕様を簡素化するものです。
暗黙ムーブ
暗黙ムーブとはC++11で許可された戻り値最適化(Return value optimization)の一種で、ローカル変数がreturn
文でコピーされて返される場合に暗黙的にムーブを行うことでコピーを回避する最適化のことです。
struct Widget { Widget(Widget&&); }; Widget one(Widget w) { return w; // ローカル変数の暗黙ムーブ、C++11から } struct RRefTaker { RRefTaker(Widget&&); }; RRefTaker two(Widget w) { return w; // ローカル変数の暗黙ムーブ、C++11(CWG1579) }
C++11では関数のローカル変数のみが暗黙ムーブの対象でしたが、C++20(P1825R0 Merged wording for P0527R1 and P1155R3)では関数ローカルの右辺値参照も暗黙ムーブ対象になったほか、return
文だけではなくthrow
式でも起こるようになり、型変換演算子等の変換を考慮するようになりました。
RRefTaker three(Widget&& w) { return w; // ローカル右辺値参照の暗黙ムーブ、C++20(P0527) } [[noreturn]] void four(Widget w) { throw w; // throw式での暗黙ムーブ、C++20(P1155) } struct From { From(Widget const &); From(Widget&&); }; struct To { operator Widget() const &; operator Widget() &&; }; From five() { Widget w; return w; // 暗黙ムーブ(コンストラクタによる変換)、C++11 } Widget six() { To t; return t; // 暗黙ムーブ(変換演算子による変換)、C++20(P1155) } struct Fowl { Fowl(Widget); // 値で受け取るコンストラクタ }; Fowl seven() { Widget w; return w; // 暗黙ムーブ、C++20(P1155) } // DerivedはBaseを公開継承しているとき Base eight() { Derived result; return result; // 暗黙ムーブ(基底クラスへの変換)、C++20(P1155) }
C++20時点の暗黙ムーブ仕様の概要
まず、暗黙ムーブ可能なもの(implicitly movable entity)とは次のどちらかです
- 自動記憶域期間の非
volatile
オブジェクト - 自動記憶域期間の非
volatile
型の右辺値参照
そして、次のどちらかのコンテキストでコピーによる初期化が行われる場合、コピーの代わりにムーブを使用して初期化することが許可されています(必須ではありません)
return
/co_return
文throw
式- オペランドはid式であり(
()
で囲まれていても良い) - そのid式の指定するもののスコープは、囲む最も内側の
try
ブロックのスコープよりも長くなく - id式は暗黙ムーブ可能なものを指定している
- オペランドはid式であり(
これらの細かい条件は、暗黙ムーブが起きた後でアクセスされる可能性のある変数を除くための条件です。
これらのコンテキストにおいて、throw
するオブジェクトを生成するためのコピーコンストラクタもしくは戻り値を生成するためのコンストラクタ、を選択するためのオーバーロード解決は次の順序で実行されます
暗黙ムーブはこの最後の手順における1において起こっており、その対象はimplicitly movable entityとして指定されます。対象外のコンテキストや暗黙ムーブが行われない場合は2の手順だけが実行されます。
C++23 P2266の概要
C++20の仕様では、暗黙ムーブが起こるのは関数の戻り値型がオブジェクト型である場合のみであり、参照型の場合は暗黙ムーブ可能なものをreturn
していても暗黙ムーブは起こりません。
int&& four(int&& w) { return w; // Error }
なぜなら、暗黙ムーブが起こるコンテキストとはコピーによる初期化が行われる場合なので、参照戻り値型の関数のreturn
文はそもそも対象外のコンテキストとなるためです。
また、C++20の暗黙ムーブの仕様は2段階のオーバーロード解決を含む複雑な処理になっており、実装が困難なことから実装による挙動の差異を生んでいました。
P2266R3ではこれらの問題の解決のために、return
文におけるムーブする資格のあるid式(move-eligible id-expression)はxvalueである、と規定することによって暗黙ムーブ仕様を簡素化します。
P2266R3では、暗黙ムーブ可能なもの(implicitly movable entity)は次のコンテキスト
return
/co_return
文throw
式- (略)
(ここは変更なし)
でid式によって指名される場合、そのid式はムーブする資格がある(move-eligible)とします。そして、ムーブする資格のあるid式の値カテゴリはxvalue
であると規定されます。
return
文でコピーによる初期化が行われるかどうかに関係なく、ムーブする資格のある変数名を指定したreturn
文はそれをxvalue
として扱う(すなわちstd::move()
したかのように扱う)ことで暗黙ムーブが行われます。また、return
文に指定された式の値カテゴリを指定した後の工程は通常のreturn
文の仕様に従うため、2段階のオーバーロード解決をする必要もなくなっています。
先程の例をもう一度見てみると
int&& four(int&& w) { return w; // 暗黙ムーブ、C++23 //return std::move(w); のような扱いになっている }
w
は暗黙ムーブ可能なもの(右辺値参照int&&
)であり、return
文ではid式w
でそれを指定しています。w
はこの関数の引数で宣言されているため(この関数スコープよりも寿命が長くはないため)このid式w
はムーブする資格のあるid式であり、値カテゴリはxvalue(すなわち、int&&
)となり、戻り値型と合うため特に変換されずにreturn
されます。
また、このような仕様の単純化によって、暗黙ムーブはされる可能性があるから必須になっています(必須になったのはこの提案より前かもしれません)。
ダングリング参照生成の抑止
P2266R3の変更によって、return
文における暗黙ムーブは(非volatile
)ローカル変数をxvalueとして扱うだけのものになり、それは常に行われます。これは関数の戻り値型に関わらずいつも行われます。
int& f() { int n = 10; return n; // ng、暗黙ムーブが起こることで、型が一致しなくなる }
すると、左辺値参照を返す関数内のreturn
文でローカル変数を直接指定すると、それは常にxvalueとして(ムーブされたかのように)扱われることとなり、T&&
をT&
で返そうとすることになる結果コンパイルエラーを起こすようになります。
これは同じメカニズムでstd:reference_wrapper
でも有効です
std::reference_wrapper<int> f() { int w; return w; // ng、C++23から }
これはreturn
文でint&& -> std::reference_wrapper<int>
の変換が起こりますが、このような変換はstd::reference_wrapper
のコンストラクタで禁止されているためです(禁止の方法はかなり複雑ですが・・・)。
ただし、間接化が1段階増えると、つまりローカル変数を参照している参照を返そうとすると、防ぐことができなくなります。
int& f() { int n = 10; int& r = n; return r; // UB } std::reference_wrapper<int> g() { int w; int& r = w; return r; // UB }
なぜかというと、どちらの場合もreturn
文で指定されているr
はローカルの左辺値参照であり、暗黙ムーブ可能なもの(implicitly movable entity)ではないためムーブする資格(move-eligible)はなく、return
文での変換はその値カテゴリのまま行われ、int&
を返そうとするためどちらの場合も問題なくコンパイルが通ってしまいます。
あくまで、左辺値参照を返す関数から直接ローカル変数を返そうとする場合にのみ保護が働きます。
提案より、その他サンプルコード
struct Weird { Weird(); Weird(Weird&); }; Weird g(bool b) { static Weird w1; Weird w2; if (b) { return w1; // OK: Weird(Weird&) } else { return w2; // error: w2はこのコンテキストでxvalue } }
// 戻り値型推論の差異 auto f(int x) -> decltype((x)) { return (x); } // 戻り値型は"int&" auto g(int x) -> decltype(auto) { return (x); } // 戻り値型は"int&&"
int& h(bool b, int i) { static int s; if (b) { return s; // OK } else { return i; // error: iはxvalue } } decltype(auto) h2(Thing t) { return t; // OK: tはxvalue、戻り値型はThing } decltype(auto) h3(Thing t) { return (t); // OK: (t)はxvalue、戻り値型はThing&& }
// Annex CセクションのC++20との非互換性レポート decltype(auto) f(int&& x) { return (x); } // int&&を返す。以前は int&を返していた int& g(int&& x) { return x; } // ill-formed; 以前は well-formed
この変更の意味するところ
P2266R3の変更が実装された(執筆時点でもclang 13/gcc 13で実装済)場合、C++規格は、コンパイラが関数内でローカル変数とそうでないものを区別できること(あるいはその能力)を仮定するようになります。これはよく考えると当たり前のことかもしれません(自動変数というカテゴリが存在しているため)が、今までこの能力が明示的に仮定されて利用されてはいなかったと思います。
この能力を利用すると、さらなるダングリング参照の抑止方法を考えることができ、既にそのような提案が提出されています。
- P2740R0 Simpler implicit dangling resolution
- P2742R0 indirect dangling identification
- P2750R0 C Dangling Reduction
P2266R3やこれらの提案が導入されてもC++が完全に安全な言語になるわけではありませんが、その安全性はわずかでも確実に向上します。