C++を使って組み込みソフトの仕事をしている三十路エンジニアですが、
恥ずかしながら最近になってムーブセマンティクスの勉強をしております。
こちらのブログで理解は進んだのですが、普段仕事で多用している参照渡しとの使い分けが
しっかりと理解できていません。
例えば、vectorの要素を2倍にする関数をstd::move
で次のように書けると思います。
cpp
1#include <vector> 2#include <utility> 3 4std::vector<int> twice_vec(std::vector<int> vec) { 5 for (auto& e : vec) { 6 e *= 2; 7 } 8 9 return std::move(vec); 10} 11 12int main(void) { 13 std::vector<int> a = {1, 2, 3}; 14 15 std::vector<int> b = twice_vec(std::move(a)); 16 17 return 0; 18}
シンタックスは明らかに異なるものの、同じ「ような」ことを参照渡しで行うと以下のように書けると思います。
cpp
1#include <vector> 2 3void twice_vec_2(std::vector<int>& vec) { 4 for (auto& e: vec) { 5 e *= 2; 6 } 7} 8 9int main(void) { 10 std::vector<int> a = {1, 2, 3}; 11 12 twice_vec_2(a); 13 14 return 0; 15}
当方の認識では、パフォーマンス的に参照渡しで書いたコードが劣ることはないと理解しています。
この差は、ムーブで書いたほうが各変数がimmutableなように書いてあって分かりやすい以外に何かあるのでしょうか。
どういう時はどっちのほうが良い、などあるのでしょうか。
(極端な話、今までポインタや参照渡しで書かれていたコードは全てムーブセマンティクスで書いたほうが良いというような話なのでしょうか)
何卒、よろしくお願いいたします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答5件
0
cpp
1std::vector<int> twice_vec(std::vector<int> vec) { 2 for (auto& e : vec) { 3 e *= 2; 4 } 5 6 return std::move(vec); 7}
このコードですがあきらかにmoveの誤用です。この場合単に
cpp
1std::vector<int> twice_vec(std::vector<int> vec) { 2 for (auto& e : vec) { 3 e *= 2; 4 } 5 6 return vec; 7}
とすればよいです。なぜならば戻り値でstd::move
をわざわざ書くと、コンパイラによるNRVOを阻害して動作を遅くするからです。return vec
とただ書けば、NRVOによってコストは0です。(ついでにいうと、clangはstd::moveつけんな、と警告を出します。)
NRVOが働かなかった時代においては、確かに引数経由で返却する習慣がありましたが、今では代入演算子の分コスト的に不利です。何も考えずにそのまま戻り値で返しましょう。
ちなみにC++17以降ではRVOが義務化され、RVOになる場合copy/move ctorが削除されていても戻り値として返却できます。
move sematicsそのものについての理解が不十分なように思えるので
みんなlvalueとrvalueを難しく考えすぎちゃいないかい?
をお読みください。
追記:
あーわかったわかった、他の人の解答見てて視点漏れしてたので解説し直し。
cpp
1void f(C& c); 2C g(const C& c);
この2つのどちらを選ぶべきか、2つの用例を考えて比較します。
immutableにしたい、なにかを元にして新規に領域を確保するようなケースでは
cpp
1void f(C& dest, const C& src); 2C g(const C& c); 3int main() 4{ 5 C src; 6 //do something 7 C dest; 8 f(src, dest); 9}
と
cpp
1C src; 2//do something 3C dest = g(src);
ではあきらかに後者を選ぶべきです。これは上で解説したようにNRVOが働くため2重copyにはならないから可読性の観点と、もしf
/g
の中でC
のコンストラクタを呼んでいてそれを変更して返却するような場合ではコピー代入演算子のコスト分お得です。私の解答はここに主眼をおいていました。
mutableにできる場合はChironianさんの解答が該当ですね。
std::moveするのって
- 関数の引数に渡して所有権を放棄するとき
ex.) std::vector::emplace_back
cpp
1f(std::move(a));
この時関数の引数の型は
cpp
1void f1(C c); 2void f2(C&& c);//rvalue reference 3template<typename CC> 4void f3(CC&& c);//universal reference(forwarding reference)
でないとmove semanticsできない(=所有権の放棄を識別できない)
f1
の場合は関数呼び出し時点でmove ctor呼び出し。内部で再度moveするなら避けるべき
f2
の場合は関数の内部でmove semanticsできる。
f3
はどちらかというとparfect forwarding
- move ctorを呼び出す時
cpp
1C c2 = std::move(c1);
が大半な気がします。
いずれにせよ場面毎に設計上の制約を把握した上で適切に判断する必要があるのでなかなか難しいですね。追記したのにうまくまとまらないし。
投稿2019/05/04 04:47
編集2019/05/04 09:13総合スコア5852
0
当方の認識では、パフォーマンス的に参照渡しで書いたコードが劣ることはないと理解しています。
正しい認識と思います。
この差は、ムーブで書いたほうが各変数がimmutableなように書いてあって分かりやすい以外に何かあるのでしょうか。
関数の“自然な”使い方は、必要な入力データを引数を介して渡し、出力データを戻り値を介して受け取るスタイルです。設計ポリシーや個人の好みはありますが、このような関数の性質(参照透過性)はプログラムの可読性や保守性といった観点から好ましいスタイルとされています。
古くからある「関数引数に参照型を用いた出力(out)引数の実現」は、実行時効率を優先したある種の"ハック"とみなせます。ムーブセマンティクスの導入により、実行時効率を犠牲にすることなく“自然な”スタイルを実現できるようになりました。
どういう時はどっちのほうが良い、などあるのでしょうか。
(極端な話、今までポインタや参照渡しで書かれていたコードは全てムーブセマンティクスで書いたほうが良いというような話なのでしょうか)
C++言語における コピー/ムーブ渡し(pass-by-value)・参照渡し(pass-by-reference)・ポインタ渡し の使い分けは、実行時効率まで考慮して最適なものを選ぼうとすると少々厄介です。実行時効率や利便性や安全性などの要素を考慮していくと、両スタイルを提供する関数オーバーロードが必要になるケースも出てきます。
網羅的な説明は難しいため、ここでは C++ Core Guidelines の紹介にとどめます。
投稿2019/05/04 06:04
総合スコア6191
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/05/04 14:01
0
パフォーマンスを気にしているようですので、取りあえず計ってみました。
ループ数を変えたり、コンパイラのバージョンを変えたり、最適化オプションを変えたりすると、あまり差が出なかったりします。ただ、twice_vec
がtwince_vec2
より明らかに速いというパターンは見つけられませんでした。書き方とか、詳しい解説は他の方にお任せします。(私には説明できないので)
そもそも、パフォーマンス以前として、twice_vec()
の方はstd::move(a)
しているので、この後のコードで、a
を参照するとコア吐いて死にます。上のコードを書くときに、値が書き換わっているかの確認とかしているとコアダンプで落ちまくるなー、なんでやー、って10分も悩みました。こういうことが起きるので、私は、よっぽどの理由が無い限りstd::move
を使わないヘタレプログラマーで生きていこうと思います。
投稿2019/05/05 10:18
総合スコア21737
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/05/05 11:18
2019/05/06 09:00
0
ベストアンサー
参照が適さないケースとして
圧縮されたデータを展開するシナリオを考えます。
c++
1std::vector<byte> inflate1(const std::vector<byte>& zipped){ 2 size_t len = *(size_t*)zipped.data(); 3 std::vector<byte> result(len); 4 5 // なんらかのデータ展開処理 6 7 return result; 8} 9 10void inflate2(const std::vector<byte> &zipped, std::vector<byte> &result){ 11 size_t len = *(size_t*)zipped.data(); 12 result.swap(std::vecor<byte>(len)); 13 14 // なんらかのデータ展開処理 15} 16 17int main(){ 18 std::vector<byte> zip{100,0,0,0,0xff,0xff}; 19 { 20 std::vector<byte> result = inflate1(zip); 21 } 22 { 23 std::vector<byte> result; 24 inflate2(zip, result); 25 } 26}
この場合、NRVOを用いたinflate1
の方がわかりやすく、効率もよいでしょう。
inflate2は、呼ばれた時点でresult
に何が入っているのか・どういう状況なのかがわかりません。
そのため、解放および初期化が必要になります。
上ではムーブセマンティクスを使いませんでした。
というのも、現状のムーブセマンティクスには欠陥があり必要でないなら使わない方がパフォーマンス上よい
と私は思っているからです。
その欠陥は、ムーブした後の残骸であろうとデストラクタが呼び出される事です。
逆に使う必要がある時というのは限られています。
それは、
- コピー代入が使えないとき
- 参照が使えないとき
です。
思いつくのは・・・
c++
1#include <iostream> 2#include <memory> 3#include <cstdlib> 4 5int main(){ 6 using namespace std; 7 unique_ptr<char> t = make_unique<char>('Q'); 8 for(int i=38;i<100;i++){ 9 unique_ptr<char> s = make_unique<char>(i); 10 11 // 適当な条件式 12 if(rand() % 100 > 93){ 13 t = std::move(s); 14 break; 15 } 16 } 17 if(t) cout << *t << endl; 18}
こんな感じでしょうか
投稿2019/05/04 10:41
編集2019/05/04 11:27総合スコア15149
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/05/04 13:46
2019/05/05 01:03
2019/05/05 02:49
2019/05/07 01:58
2019/05/07 14:16
2019/05/08 03:12
0
こんにちは。
当方の認識では、パフォーマンス的に参照渡しで書いたコードが劣ることはないと理解しています。
微差ですが、参照渡しの方がオーバーヘッドは少ないと思います。
ムーブといっても移動できるものは「所有権≒解放する義務」に過ぎません。所有権を移動できないようなリソースは普通にコピーされます。例えば、std::vectorの場合、「要素数」や「要素を獲得した領域へのポインタ」等は移動できないので普通にコピーされます。ムーブ(所有権を移動)されるのは要素の値を保持しているメモリ(一般にヒープ・メモリ)だけです。
この差は、ムーブで書いたほうが各変数がimmutableなように書いてあって分かりやすい以外に何かあるのでしょうか。
std::moveを指定するということはmmutable許可ですよ。こっそりムーブされるstd::auto_ptrの反省からこっそりムーブされると困る時はムーブ許可を明示することになったというもので、呼び出し先での変更を許可するという意味も込められています。
また、正直、「右辺値参照」を理解することの難易度は高いと思います。
更に、std::moveの必要性を理解せずにstd::vector<int> b = twice_vec(a);
と書かれると泣きたくなるかも。
従って、下記3つの理由で左辺値参照を使った方が好ましいと感じます。
- より知識が浅い人でも理解できるし書くこともできる
- パフォーマンス的に微差とは言え有利
- 右辺値参照版はstd::moveを書き忘れるとstd::vectorのコピーが発生するので悲しい
なお、下記のように定義すれば、上記の3.を回避できます。
C++
1std::vector<int> twice_vec(std::vector<int>&& vec) { 2 for (auto& e : vec) { 3 e *= 2; 4 } 5 6 return std::move(vec); 7}
このケースでは、returnのstd::moveは書いた方がいいような気がします。
NRVOは機能できない筈ですし、構文的にはコピーになる筈です。コンパイラがstd::moveなしでも左辺値をムーブしてくれればよいのですが。
↓右辺値参照は左辺値です。わけわからんですね。
https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html
右辺値参照で宣言された変数は右辺値ではなく、左辺値である。
投稿2019/05/04 08:10
編集2019/05/04 08:27総合スコア23272
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/05/04 08:27
2019/05/04 08:30
2019/05/04 13:22 編集
2019/05/04 13:36
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/05/04 06:29 編集
2019/05/04 13:19