質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.49%
C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

3回答

3667閲覧

以下のrange-based forは危険でしょうか。

__ook

総合スコア49

C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

1グッド

2クリップ

投稿2019/12/17 00:45

編集2019/12/19 02:35

以下の記事を読んでいたところ
https://qiita.com/rinse_/items/ad0cc7e351e836595c94

cpp

1// 危険なコード 2for( auto e : something().get_vector() ) // get_vectorは内部に持つvectorへの参照を返す 3{ 4 // e is destroyed 5} 6 7// 安全なコード 8for( auto e : std::vector < int > { 1,2,3,4,5,6,7,8,9,0 } ) 9{ 10 // e is 1,2,3,...,0 11}

range-based forでは、コンテナへの参照をキャプチャします。上の危険な例ではキャプチャしたものがすぐに破棄されてしまいますが、下の安全な例では、一時オブジェクトをキャプチャして延命しているので安全に利用することができるのです。

という文言があったため、試してみたところ、正しく動いているように見えました。
これはなぜ動いてしまうのでしょう。
環境はVisual Studio 2015です。

また、

cpp

1#include <iostream> 2#include <fstream> 3#include <string> 4#include <vector> 5 6using namespace std; 7 8auto retVec(vector<string> original) 9{ 10 return original; 11} 12 13struct str 14{ 15 vector<string> _vec 16 { 17 "first", 18 "second", 19 "third" 20 }; 21public: 22 auto get_vec() { return _vec; }; 23}; 24 25int main() 26{ 27 for (auto e : str().get_vec()) // 危険? 28 { 29 cout << e << endl; 30 } 31 32 str str_exist; 33 for (auto e : str_exist.get_vec()) // これも危険? 34 { 35 cout << e << endl; 36 } 37}

2つ目のrange-based forは危険でしょうか?

【追記】

cpp

1 2#include <iostream> 3#include <fstream> 4#include <string> 5#include <vector> 6 7using namespace std; 8 9 10struct str 11{ 12 vector<int> _vec 13 { 14 0, 15 1, 16 2 17 }; 18public: 19 auto& get_vec() { return _vec; } 20}; 21 22int main() 23{ 24 for (auto e : str().get_vec()) 25 { 26 cout << e << endl; 27 } 28}

上記なら危険なのですね。

yumetodo👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

t_obara

2019/12/17 01:25

とりあえず、参照されているページと同様、デストラクタを印字して確かめてみてはいかがですか?
guest

回答3

0

ベストアンサー

前提

まずRange-based forがどのように展開されるか確認しましょう。

§9.5.4 [stmt.ranged]/1
The range-based for statement

cpp

for ( for-range-declaration : for-range-initializer ) statement

is equivalent to

cpp

{
auto &&__range = for-range-initializer ;
auto __begin = begin-expr ;
auto __end = end-expr ;
for ( ; __begin != __end; ++__begin ) {
for-range-declaration = *__begin;
statement
}
}

そしてそれぞれ見ていきましょう。

1つ目の例

cpp

1for( 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

1std::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つ目と同様なので割愛します。

投稿2019/12/17 04:53

編集2019/12/17 05:24
yumetodo

総合スコア5850

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yohhoy

2019/12/17 10:40 編集

> ちなみにメンバー関数の宣言時にref qualifierをつけるとコンパイル時にこのようなことを防げます。 好ましいかといわれると微妙(やりすぎ)な設計な気もしますが、ref qualifierによるオーバーロードを活用して左辺値からはデータメンバへの左辺値参照を/右辺値からはムーブされたデータメンバを返すこともできますね。 https://wandbox.org/permlink/7fFSaBCQm81kOKsP
yumetodo

2019/12/17 16:00

ああ、そういえばreturn文でのstd:moveは常に間違ってるってずっと説明してきたのですが、この例では正しい用法になるのか・・・。
__ook

2019/12/19 02:11

回答ありがとうございます… なるほど、僕が真似た(つもりだった)コードはそもそもコピーを返しているから安全なのですね、読み違えていました。
guest

0

結論からいえば、質問文中コードの場合は両range-based forともに「安全」です。

c++

1struct str 2{ 3 // (略) 4 auto get_vec() { return _vec; }; 5};

strクラス(構造体)get_vecメンバ関数が、データメンバ_vecへの参照ではなくコピー結果を一時オブジェクトとして返すインタフェースのため、参考にされたQiita解説記事とは異なる結果になっています。


もし、get_vecメンバ関数がデータメンバへの参照を返すインタフェース(例えば戻り値型がauto&)だとすると:

  • 前者str().get_vec()は「危険」です。一時オブジェクトstr()とともに参照先vector<string>実体も破棄されてしまうため、走査中のイテレータは無効オブジェクトを指してしまいます。
  • 後者str_exist.get_vec()は「安全」です。変数str_existの生存期間中は参照先も生存していますから、安全にvector<string>実体へとアクセスできます。

余談:個人の感性にも左右されますが、get_vecというメンバ関数名からはデータメンバへの参照を返すインタフェースを想起させますし、効率的なC++クラス設計としても参照を返す方がパフォーマンス上好ましいです。本当に「コピーされたデータを返す」のであれば、clonecopyなどの名称を含めたほうがよいかもしれません。

投稿2019/12/17 03:55

編集2019/12/17 03:59
yohhoy

総合スコア6191

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

__ook

2019/12/19 02:13

すごく理解できました。ありがとうございます。 > 余談:個人の感性にも左右されますが、get_vecというメンバ関数名からはデータメンバへの参照を返すインタフェースを想起させますし、効率的なC++クラス設計としても参照を返す方がパフォーマンス上好ましいです。 なるほど…参照を返却すべきなのですね、ただその場合はrange-based forを使用することは望ましくない、と…
yohhoy

2019/12/20 01:57

これはクラス設計の問題ですから、明確な正解は存在しないと思います。 yumetodoさん回答のコメント部に書いたように、例えば https://wandbox.org/permlink/7fFSaBCQm81kOKsP という解法もありえます。 上記では参照返却による効率性と、ムーブされたオブジェクト返却による安全性を両立させたものになっています。(代償として、より高度なC++の知識を要求することにはなりますが...)
__ook

2019/12/20 02:26

なるほど…たしかにこれなら安全に一時オブジェクトを参照できますね…
guest

0

こんにちは。

私もあれ?っと思ったのですが、どうも範囲ベースforのコンテナへの参照は右辺値参照で参照されるからのようです。
この旨のコードが範囲for文に記載されていました。

従って、ここで回答したのと同じ原理で破棄されてしまうということのようです。

wandboxで実験のコードの前者は上記質問のNG1と同じ原理です。

2つ目のrange-based forは危険でしょうか?

str_existの寿命はmain関数の終わりまであるので、危険ではないです。
これに対して、for文の「コンテナを指定する式の一時オブジェクト」の寿命はその式が終わるまでですので、for文のループ・ブロックに入る前に破棄されます。

投稿2019/12/17 03:41

編集2019/12/17 03:53
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

__ook

2019/12/19 02:16

ああ…なるほど…ありがとうございます。 つまり…参照で返却した場合iteratorで回すのが正、でしょうか?
Chironian

2019/12/19 02:38

iteratorだろうが範囲ベースだろうが、ループ対象のコンテナ・オブジェクトがfor文の終わりまで生きているようなコードならOKです。 iteratorで、beginとendのオブジェクトについて、それぞれ異なる一時オブジェクトを指定する人はいないとは思いますが、油断するとやるかもですね。例えば下記はダメです。 for( auto it = something().get_vector().begin(); it != something().get_vector().end(); ++it) { }
__ook

2019/12/19 04:15

やったことがあるやつでした… ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問