前提
まずRange-based forがどのように展開されるか確認しましょう。
§9.5.4 [stmt.ranged]/1
The range-based for statement
for ( for-range-declaration : for-range-initializer ) statement
is equivalent to
{
auto &&__range = for-range-initializer ;
auto __begin = begin-expr ;
auto __end = end-expr ;
for ( ; __begin != __end; ++__begin ) {
for-range-declaration = *__begin;
statement
}
}
そしてそれぞれ見ていきましょう。
1つ目の例
cpp
1 for ( auto e : something ( ) . get_vector ( ) ) // get_vectorは内部に持つvectorへの参照を返す
2 {
3 // e is destroyed
4 }
危険です。__range
の型はstd::vector<int>&
になります。このときsomething()
の寿命は尽きています。つまり存在しないオブジェクトを束縛しています。
https://wandbox.org/permlink/v0c1cR7NzOfaPydd
Linux上でAddressSanitizerに掛けると
./a.out
destructor
=================================================================
==5022==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffd5b3fe1e0 at pc 0x0000004c70dd bp 0x7ffd5b3fe070 sp 0x7ffd5b3fe068
READ of size 8 at 0x7ffd5b3fe1e0 thread T0
#0 0x4c70dc in __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >::__normal_iterator(int* const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_iterator.h:783:20
#1 0x4c5eed in std::vector<int, std::allocator<int> >::begin() /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_vector.h:564:16
#2 0x4c57f2 in main /home/yumetodo/teratail/229984/example1/main.cpp:17:15
#3 0x7f69dc193b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#4 0x41b659 in _start (/home/yumetodo/teratail/229984/example1/a.out+0x41b659)
Address 0x7ffd5b3fe1e0 is located in stack of thread T0 at offset 32 in frame
#0 0x4c558f in main /home/yumetodo/teratail/229984/example1/main.cpp:15
This frame has 5 object(s):
[32, 56) 'ref.tmp' (line 17) <== Memory access at offset 32 is inside this variable
[96, 112) 'ref.tmp1' (line 17)
[128, 168) 'ref.tmp2' (line 17)
[208, 216) '__begin1' (line 17)
[240, 248) '__end1' (line 17)
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_iterator.h:783:20 in __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >::__normal_iterator(int* const&)
Shadow bytes around the buggy address:
0x10002b677be0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10002b677bf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10002b677c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10002b677c10: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00 f3 f3 f3
0x10002b677c20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10002b677c30: 00 00 00 00 00 00 00 00 f1 f1 f1 f1[f8]f8 f8 f2
0x10002b677c40: f2 f2 f2 f2 f8 f8 f2 f2 f8 f8 f8 f8 f8 f2 f2 f2
0x10002b677c50: f2 f2 00 f2 f2 f2 f8 f3 f3 f3 f3 f3 00 00 00 00
0x10002b677c60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10002b677c70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10002b677c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==5022==ABORTING
のように言われます。(MicrosoftがMSVCに試験実装したASan は何も言ってこない、Releaseビルドでしか使えないから最適化でUBだから吹き飛んでるので見つからないんじゃないかと推測)
ちなみにメンバー関数の宣言時にref qualifierをつけるとコンパイル時にこのようなことを防げます。
cpp
1 std :: vector < int > & get_vector ( ) & { return ( _Value ) ; }
prog.cc: In function 'int main()':
prog.cc:17:62: error: passing 'something' as 'this' argument discards qualifiers [-fpermissive]
17 | for( auto e : something { 1,2,3,4,5,6,7,8,9,0 }.get_vector() ) // get_vectorは内部に持つvectorへの参照を返す
| ^
prog.cc:10:24: note: in call to 'std::vector<int>& something::get_vector() &'
10 | std::vector < int >& get_vector() & { return ( _Value ); }
| ^~~~~~~~~~
https://wandbox.org/permlink/eRayizx8KL07dgja
2つ目の例
cpp
1 for ( auto e : std :: vector < int > { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 } )
2 { // 一時オブジェクトはキャプチャされ延命されている
3 std :: cout << e ;
4 }
安全です。__range
の型はstd::vector<int>&&
になります。このとき一時オブジェクトはrvalue referenceによって束縛されているので寿命が延長されます。
https://wandbox.org/permlink/C6vnwrPNcDHazhot
3つ目の例
cpp
1 for ( auto e : str ( ) . get_vec ( ) ) // 危険?
2 {
3 cout << e << endl ;
4 }
安全です。ここでget_vec
の戻り値の型がstd::vector<std::string>
であることに注意してください。つまりこの関数はメンバー変数の参照ではなくてコピーを返しています。したがって2つめの例と同様のことが起こります。
https://wandbox.org/permlink/cUE16XoExbGq6X1T
4つ目の例
cpp
1 str str_exist ;
2 for ( auto e : str_exist . get_vec ( ) ) // これも危険?
3 {
4 cout << e << endl ;
5 }
安全です。3つ目と同様なので割愛します。