はじめに
気を付けていても、ふとした瞬間に右辺値参照の寿命を切らすことがあります。
これはコンパイラの種別・バージョン・最適化オプションによって、たまたま動いたり動かなかったりするので結構しんどい問題です。
今回、平成最後のアドベントカレンダーということもあり、いや関係ないですが、恥を忍んで実際に寿命を切らしてしまった事例と対策を紹介します。
事例1: std::minmax で殺られた
auto mm = std::minmax(5, 6);
std::cout << "min:" << mm.first << " max:" << mm.second << std::endl;
結果はどうなるでしょうか? min:5 max:6
が印字されそうですが...
答えは不定です。
http://rextester.com/DKK11270
どうしてこうなった
std::minmax のシグニチャはこうです:
template< class T >
std::pair<const T&,const T&> minmax( const T& a, const T& b );
引数で与えた数値 5, 6 はrvalue(メモリ上に実体がない値)オブジェクトです。
これがconst int&
で参照されたので、このminmax()
を呼び出している文の終わり、つまり次のセミコロンまでは、寿命が伸ばされます。
さて、minmax()
の中に入りました。数値5と6への参照をさらにstr::pair
のコンストラクタに参照として渡します。
template<typename T>
pair<const T&, const T&>
minmax(const T& a, const T& b)
{
return b < s ? pair<const T&, const T&>(b, a) : pair<const T&, const T&>(a, b);
}
作られたpairオブジェクトは、渡された 数値への参照をどうしているかというと...
template<typename T1, typename T2>
struct pair
{
T1 first;
T2 second;
:
};
今、型T1, T2はconst int&
なので、実体を保持していません。
intの数値実体を持っていないpairオブジェクトがminmax()
から返され、それがauto mm
にコピーされます。
auto mm = std::minmax(5, 6);
この mm は実際にはどんな形になるかというと、minmax()
のシグニチャよりstd::pair<const T&, const T&>
です。やはり実体持っていません。
そうこうしているうちに minmax()
呼び出し行が終了し、 一時オブジェクトの 5 と 6 が破棄されました。
mmは破棄された 5, 6の跡地を参照していますので、
std::cout << "min:" << mm.first << " max:" << mm.second << std::endl;
この結果がどうなるかは運次第、という状況になります。
一連の流れをザラッと図にしてみました。
直し方
const参照のpairでなく実体のpairで受けると、文が終わる前にコピーされるので大丈夫になります。(上の図で言うと4のタイミングで実体がコピーされる)
std::pair<int, int> mm = std::minmax(5, 6);
std::cout << "min:" << mm.first << " max:" << mm.second << std::endl;
ちなみにstd::min
, std::max
だったら、auto
で受けても大丈夫です。
std::min()
の戻り値 const T&
をauto
でうけると、auto
は T
に推論(今の例で言えばint
に推論)され、値がコピーされることになります。
実際のしにざま
きれいな顔してるだろ。ウソみたいだろ。死んでるんだぜ。
int main()
{
std::vector<int> v1{0,1,2,3,4}; // size(): 5
std::vector<int> v2{5,6,7,8,9,10}; // size(): 6
auto mm = std::minmax(v1.size(), v2.size());
std::cout << "min:" << mm.first << " max:" << mm.second << std::endl;
対策
コンパイラは警告を吐いてくれませんでした。
が、gcc7では以下のオプションでランタイムエラーを吐いてくれました。
単体テストなりで1回通せば気づけます。
g++-7 -fsanitize=address -fsanitize-address-use-after-scope
みんな知ってたの?
ちゃんと書いてあります...
Notes
For overloads (1,2), if one of the parameters is an rvalue, the reference returned becomes a dangling
reference at the end of the full expression that contains the call to minmax:
ここはまずいですね。。。
http://kaworu.jpn.org/cpp/std::minmax
おお、ここはautoで受けていない!老獪か!
https://cpprefjp.github.io/reference/algorithm/minmax.html
事例2: メソッドチェインでやられる
struct Test {
Test& inc() {
++value;
return *this;
}
int value = 1;
};
int main() {
auto&& t = Test().inc();
std::cout << t.value << std::endl;
return 0;
}
結果はどうなるでしょうか? 2が返ってきそうですが。。。
gccでは0になりました。
アライさんまた殺ってしまったね
順に追っていきましょう。
まず、下記は右辺値参照です。Test()は Testオブジェクトの一時オブジェクトを作り、それがtに束縛さるので、この後もちゃんと使えます。
auto&& t = Test(); // OK. decltype(t) == Test&&
つぎに、調子に乗ってinc()メソッドも呼んでみます。
auto&& t = Test().inc(); // NG. decltype(t) == Test&
一時オブジェクトは、文が終わるまで有効ですから、inc()は安全に呼べます。
そのinc()は Test& 型を返します。
auto&&
の型決定時、これは一時オプジェクトではなく、実体のある左辺値への参照として解釈され、 t も左辺値への参照、すなわち Test& となります。
その結果、誰も Testオブジェクトを持つ人がいなくなるため、
std::cout << t.value << std::endl; // ガッ!
残念な結果に終わります。
直し方
素直に分けましょう。
auto t = Test():
t.inc();
2017-12-10追記: コメント欄にて、@yumetodoさんからref qualifierを使って安全にmoveするやり方を教えて頂きました。自分的に咀嚼し切れてないので、とりあえずベストな選択は各自の演習とします
2018-01-20追記: 上記指摘頂いた方法でも問題ありとのことです。
ありがとうございます> @akinomyoga さん @yumetodo さん
例によって私自身、規格の理解が追いついていないので、とりあえずベス(ry
頑張って理解後、説明書きたい...
対策
事例1と同様、gccのランタイムチェッカにお願いすると、優しく殺してくれます。
まとめ
今回、ダングリング参照が起こる事例を紹介しました。
C++初心者向けカレンダーにふさわしいのかどうか分かりませんが、実際問題として、知らないと致命的な不具合の元になる内容です。
コンパイラやvalgrindなどのツールを活用して、安全なコードを書いていきましょう。
御安全に!