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

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

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

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

参照

参照は、プログラミングにおいて変数や関数といったメモリ空間上での所在を指示するデータのことを指します。その中にはデータ自体は含まれず、他の場所にある情報を間接的に指示するプログラムです。

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

C++

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

Q&A

解決済

5回答

16701閲覧

C++ 参照渡し vs ムーブセマンティクス

sin_250

総合スコア112

C++11

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

参照

参照は、プログラミングにおいて変数や関数といったメモリ空間上での所在を指示するデータのことを指します。その中にはデータ自体は含まれず、他の場所にある情報を間接的に指示するプログラムです。

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

C++

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

1グッド

4クリップ

投稿2019/05/04 04:07

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なように書いてあって分かりやすい以外に何かあるのでしょうか。
どういう時はどっちのほうが良い、などあるのでしょうか。
(極端な話、今までポインタや参照渡しで書かれていたコードは全てムーブセマンティクスで書いたほうが良いというような話なのでしょうか)

何卒、よろしくお願いいたします。

nishiys👍を押しています

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

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

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

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

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

guest

回答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するのって

  1. 関数の引数に渡して所有権を放棄するとき

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

  1. move ctorを呼び出す時

cpp

1C c2 = std::move(c1);

が大半な気がします。

いずれにせよ場面毎に設計上の制約を把握した上で適切に判断する必要があるのでなかなか難しいですね。追記したのにうまくまとまらないし。

投稿2019/05/04 04:47

編集2019/05/04 09:13
yumetodo

総合スコア5850

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

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

yohhoy

2019/05/04 06:29 編集

(補足&言い訳をば) > このコードですがあきらかにmoveの誤用です。 質問者が参照された記事はムーブセマンティクス初学者向けの説明に徹したため、意図的にreturn文でもmoveを明示しました。これは同記事脚注でも少し説明しています。 また2012年当時はC++11未対応コンパイラも多く、moveを明示した方が「安全」だろうという考えもありました。(コンパイラが古くてコピーが選択されるリスク > NRVOが阻害され常にムーブ処理となるデメリット) yumetodoさん指摘の通り、2019年現在のC++コンパイラはこのmove関数呼び出しが冗長かつ有害と警告するようになっています。
sin_250

2019/05/04 13:19

ご回答有り難うございます。 (返信に時間がかかって申し訳ないです、頂いた回答を消化するのに時間がかかっています) > std::moveをわざわざ書くと、コンパイラによるNRVOを阻害して動作を遅くするからです。return vecとただ書けば、NRVOによってコストは0です これは、return時にstd::move(vec)すると、呼び出し元の変数に代入するときにムーブ代入演算子のコストが発生する。 NRVOが働けばコピーもムーブも発生しないという意味ですね。 (恥ずかしながらNRVOもわかっておりませんでした・・・) > f(src, dest);とC dest = g(src);ではあきらかに後者を選ぶべきです 元々これが質問時に頭にあったことです。 つまり、今日ムーブセマンティクスを勉強していて、ムーブを使うとdest = g(src)的な書き方を - コピーを発生させず - かつ破壊的変更をせずに 書けるのがメリットだと理解しました。 ただ、正しくは上記は、値返しの書き方で、NRVOを使えばそれでも実現できる、ということですね。 (asmさんが書かれたinflate1関数のように) 参照返しでもできるかな?と思って調べましたが、やっぱりできないという理解を、今はしています。 (関数内で作った自動変数の参照を返すことは基本的に出来ない) (関数内でnewしてポインタを返せば出来なくはないが、deleteがややこしい) ちょっと私が混乱していてまだ理解が整理できていないため、少し時間をください。 (完全転送など、ちょっとまだ理解てきていない) ご回答有り難うございました。
guest

0

当方の認識では、パフォーマンス的に参照渡しで書いたコードが劣ることはないと理解しています。

正しい認識と思います。

この差は、ムーブで書いたほうが各変数がimmutableなように書いてあって分かりやすい以外に何かあるのでしょうか。

関数の“自然な”使い方は、必要な入力データを引数を介して渡し、出力データを戻り値を介して受け取るスタイルです。設計ポリシーや個人の好みはありますが、このような関数の性質(参照透過性)はプログラムの可読性や保守性といった観点から好ましいスタイルとされています。

古くからある「関数引数に参照型を用いた出力(out)引数の実現」は、実行時効率を優先したある種の"ハック"とみなせます。ムーブセマンティクスの導入により、実行時効率を犠牲にすることなく“自然な”スタイルを実現できるようになりました。

どういう時はどっちのほうが良い、などあるのでしょうか。
(極端な話、今までポインタや参照渡しで書かれていたコードは全てムーブセマンティクスで書いたほうが良いというような話なのでしょうか)

C++言語における コピー/ムーブ渡し(pass-by-value)・参照渡し(pass-by-reference)・ポインタ渡し の使い分けは、実行時効率まで考慮して最適なものを選ぼうとすると少々厄介です。実行時効率や利便性や安全性などの要素を考慮していくと、両スタイルを提供する関数オーバーロードが必要になるケースも出てきます。

網羅的な説明は難しいため、ここでは C++ Core Guidelines の紹介にとどめます。

投稿2019/05/04 06:04

yohhoy

総合スコア6191

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

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

sin_250

2019/05/04 13:29

まさか勉強していたブログのご本人様から回答を頂けるとは思ってませんでした汗 >このような関数の性質(参照透過性)はプログラムの可読性や保守性といった観点から好ましいスタイルとされています。 あまりちゃんと理解できていませんが、関数型言語のように破壊的変更を許さないように したほうが安全だから取り込んでいこう、というトレンドがあるのは感じております。 >実行時効率まで考慮して最適なものを選ぼうとすると少々厄介です 私自身が個人的に実現したいことは、今仕事でよく扱うデータがグリッド地図などを表現する 2次元vectorを扱うことが多いため、 - vectorの愚直な値コピーは発生させない - それさえ守れれば後は可読性が高く、バグが出づらいコードを書きたい になっています。なので、微差な計算コストは許容できる、 それより分かりやすさを優先したいと考えています。 ご紹介いただいたCore Guidelines、チラ見した限り更に混乱しましたが、(笑 もう少し頑張ってみます・・・!
yohhoy

2019/05/04 14:01

とりあえず網羅的に載っているので C++ Core Guidelines を紹介しはしましたが、内容更新のアクティビティがやたら高い(≒安定してない)のと、無邪気に最新規格(策定前のC++20等)を前提にしていたりと、現実の業務利用においてはあくまで参考程度で見てください。 ムーブセマンティクスはあくまでも「関数入出力設計の選択肢が増えた」にすぎませんから、既存コードベースとの整合性やチームメンバの練度と相談しながら導入していくのが現実解とは思います。 > まさか勉強していたブログ 多少なりともお役に立っていればなによりです :D
guest

0

パフォーマンスを気にしているようですので、取りあえず計ってみました。


ループ数を変えたり、コンパイラのバージョンを変えたり、最適化オプションを変えたりすると、あまり差が出なかったりします。ただ、twice_vectwince_vec2より明らかに速いというパターンは見つけられませんでした。書き方とか、詳しい解説は他の方にお任せします。(私には説明できないので)

そもそも、パフォーマンス以前として、twice_vec()の方はstd::move(a)しているので、この後のコードで、aを参照するとコア吐いて死にます。上のコードを書くときに、値が書き換わっているかの確認とかしているとコアダンプで落ちまくるなー、なんでやー、って10分も悩みました。こういうことが起きるので、私は、よっぽどの理由が無い限りstd::moveを使わないヘタレプログラマーで生きていこうと思います。

投稿2019/05/05 10:18

raccy

総合スコア21735

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

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

yumetodo

2019/05/05 11:18

やっぱりNRVO有能、全要素copy入る分不利なはずなのに、lvalue reference版とそこまでかわらない
sin_250

2019/05/06 09:00

ありがとうございます、確かにreturnでmoveする場合としない場合で顕著に違いました。 本業では左辺値参照の出力用仮引数を使う書き方でしばらくは続けたいと思います。 組み込み用のレガシーな世界にいるので、移植性やメンバの練度の観点で、RVO/NRVOを前提とした実装を職場でacceptしてもらえるかは検討が必要そうです。が、個人の趣味プログラミングでは使っていきたいと思います。 色々な方から有益な情報をもらいまして、正直ベストアンサーを決められない状態になってしまいました。 ただ、自分のレベルに適した回答とNRVOのテスト例をしていただけたasmさんにこの場ではさせていただきたいと思います。 みなさんのTwitterやQiitaの記事も参考になることが多く、今も大変助かっています。ありがとうございました。
guest

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
asm

総合スコア15147

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

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

sin_250

2019/05/04 13:46

ご回答有り難うございます。 inflate1とinflate2の違いの説明が本来聞きたかった(が質問が下手で聞けてなかった)ことです。 inflate1で値渡しで返す書き方は、返す際にコピーが発生するから、やってはダメなやり方だと思っていたのですが、 NRVOが働く状況ではむしろ正しいのやり方なのですね。 (ちなみに、NRVOが働くか否かはコンパイラ依存だと思いますが、 働いているかどうか確認する術はあるのか気になって調べています) 180度認識が変わったので大きく勉強になりました。 unique_ptrがコピー不可なところでmoveが登場するという流れは何となく読んだのですが、 unique_ptrもちゃんと理解していないところなので、引き続き勉強します。 (いわゆる大手メーカでレガシーなコードを書きがちなので・・・お恥ずかしい) ありがとうございます。
asm

2019/05/05 01:03

個人的にモダンなC++とレガシーなC++を分断する革新的な最適化だと思ってます。>RVO (N)RVOが働いているかは、私の知る限り実行前に知ることはできなかったと思います。 デストラクタ等を省略するという観測可能な副作用があるので実験することは可能です。 https://wandbox.org/permlink/cDXZkHXbdVJtLT0K
sin_250

2019/05/05 02:49

コード例ありがとうございます。NRVOを無効化するオプションをつけて、 コピーコンストラクタのデバッグログも追加して試したら違いが視認できました。感謝します。 https://wandbox.org/permlink/CY9lSZjVytUzIRCw
yohhoy

2019/05/07 01:58

一応補足しておくと、RVOやNRVOはC++03時点から存在していた言語仕様です。(C++17以前は)C++コンパイラの裁量で最適化してもよい/しなくてもよいという仕様ですね。
sin_250

2019/05/07 14:16

>> yohhoyさん 重ね重ね、ありがとうございます。現状、もしRVOが適用されなかった場合に簡単に検出できないのが少し怖いなぁと思ってとりあえず置いとこうかなと考えています。コピーコンストラクタを=deleteにしたらどうかなと思って試したらそれは単なるコンパイルエラーになってしまうしで、なかなか難しいなぁと感じています。 しかしながら、今回のQAで知らなかった様々なことを知ることが出来たため、本当に感謝です。
yohhoy

2019/05/08 03:12

> もしRVOが適用されなかった場合に簡単に検出できないのが少し怖い RVO/NRVOは文字通り最適化(Optimization)の一種なので、プログラムがその適用有無に依存する=設計不良の兆候かもしれません。 クラスの{コピー|ムーブ}{コンストラクタ|代入演算子}ではオブジェクトの複製/移動操作のみを行うべきであり、RVO/NRVO有無で外部観測可能な実行結果が変化するプログラム設計は望ましくないと思います。
guest

0

こんにちは。

当方の認識では、パフォーマンス的に参照渡しで書いたコードが劣ることはないと理解しています。

微差ですが、参照渡しの方がオーバーヘッドは少ないと思います。
ムーブといっても移動できるものは「所有権≒解放する義務」に過ぎません。所有権を移動できないようなリソースは普通にコピーされます。例えば、std::vectorの場合、「要素数」や「要素を獲得した領域へのポインタ」等は移動できないので普通にコピーされます。ムーブ(所有権を移動)されるのは要素の値を保持しているメモリ(一般にヒープ・メモリ)だけです。

この差は、ムーブで書いたほうが各変数がimmutableなように書いてあって分かりやすい以外に何かあるのでしょうか。

std::moveを指定するということはmmutable許可ですよ。こっそりムーブされるstd::auto_ptrの反省からこっそりムーブされると困る時はムーブ許可を明示することになったというもので、呼び出し先での変更を許可するという意味も込められています。

また、正直、「右辺値参照」を理解することの難易度は高いと思います。
更に、std::moveの必要性を理解せずにstd::vector<int> b = twice_vec(a);と書かれると泣きたくなるかも。

従って、下記3つの理由で左辺値参照を使った方が好ましいと感じます。

  1. より知識が浅い人でも理解できるし書くこともできる
  2. パフォーマンス的に微差とは言え有利
  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
Chironian

総合スコア23272

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

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

yumetodo

2019/05/04 08:27

>更に、std::moveの必要性を理解せずにstd::vector<int> b = twice_vec(a);と書かれると泣きたくなるかも。 一ミリもmoveが必要ないんですがそれは
Chironian

2019/05/04 08:30

vector変数aがムーブではなく、コピーされますよ。 twice_vec_2よりパフォーマンスが格段に落ちます。文脈的にそれは「なし」と理解しています。
yumetodo

2019/05/04 13:22 編集

immutableにしたいって話だとおもったんだけどどうも視点漏れしてたので今解答書き直してます。
sin_250

2019/05/04 13:36

>std::moveの必要性を理解せずにstd::vector<int> b = twice_vec(a);と書かれると泣きたくなるかも。 おっしゃるように、今の私のチームでムーブセマンティクスを使ったコードを書いたら 「なんで値渡しする関数書いてるの!?」とマサカリの的になると思います。 チームでコンセンサスを取って使わないと混乱しそうです。 一応、&&で関数の引数を右辺地参照に明示的に限定することもできるのですね。 まだ理解が浅いので、仕事で使うのは控えておくようにします。 ただ、理解はできるようがんばります。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問