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

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

ただいまの
回答率

89.07%

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

解決済

回答 5

投稿

  • 評価
  • クリップ 4
  • VIEW 3,360

sin_250

score 108

C++を使って組み込みソフトの仕事をしている三十路エンジニアですが、
恥ずかしながら最近になってムーブセマンティクスの勉強をしております。

こちらのブログで理解は進んだのですが、普段仕事で多用している参照渡しとの使い分けが
しっかりと理解できていません。

例えば、vectorの要素を2倍にする関数をstd::moveで次のように書けると思います。

#include <vector>
#include <utility>

std::vector<int> twice_vec(std::vector<int> vec) {
  for (auto& e : vec) {
    e *= 2;
  }

  return std::move(vec);
}

int main(void) {
  std::vector<int> a = {1, 2, 3};

  std::vector<int> b = twice_vec(std::move(a));

  return 0;
}

シンタックスは明らかに異なるものの、同じ「ような」ことを参照渡しで行うと以下のように書けると思います。

#include <vector>

void twice_vec_2(std::vector<int>& vec) {
  for (auto& e: vec) {
    e *= 2;
  }
}

int main(void) {
  std::vector<int> a = {1, 2, 3};

  twice_vec_2(a);

  return 0;
}

当方の認識では、パフォーマンス的に参照渡しで書いたコードが劣ることはないと理解しています。
この差は、ムーブで書いたほうが各変数がimmutableなように書いてあって分かりやすい以外に何かあるのでしょうか。
どういう時はどっちのほうが良い、などあるのでしょうか。
(極端な話、今までポインタや参照渡しで書かれていたコードは全てムーブセマンティクスで書いたほうが良いというような話なのでしょうか)

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 5

+4

std::vector<int> twice_vec(std::vector<int> vec) {
  for (auto& e : vec) {
    e *= 2;
  }

  return std::move(vec);
}

このコードですがあきらかにmoveの誤用です。この場合単に

std::vector<int> twice_vec(std::vector<int> vec) {
  for (auto& e : vec) {
    e *= 2;
  }

  return vec;
}

とすればよいです。なぜならば戻り値でstd::moveをわざわざ書くと、コンパイラによるNRVOを阻害して動作を遅くするからです。return vecとただ書けば、NRVOによってコストは0です。(ついでにいうと、clangはstd::moveつけんな、と警告を出します。)

NRVOが働かなかった時代においては、確かに引数経由で返却する習慣がありましたが、今では代入演算子の分コスト的に不利です。何も考えずにそのまま戻り値で返しましょう。

ちなみにC++17以降ではRVOが義務化され、RVOになる場合copy/move ctorが削除されていても戻り値として返却できます。


move sematicsそのものについての理解が不十分なように思えるので
みんなlvalueとrvalueを難しく考えすぎちゃいないかい?
をお読みください。


追記:

あーわかったわかった、他の人の解答見てて視点漏れしてたので解説し直し。

void f(C& c);
C g(const C& c);

この2つのどちらを選ぶべきか、2つの用例を考えて比較します。

immutableにしたい、なにかを元にして新規に領域を確保するようなケースでは

void f(C& dest, const C& src);
C g(const C& c);
int main()
{
    C src;
    //do something
    C dest;
    f(src, dest);
}

C src;
//do something
C dest = g(src);

ではあきらかに後者を選ぶべきです。これは上で解説したようにNRVOが働くため2重copyにはならないから可読性の観点と、もしf/gの中でCのコンストラクタを呼んでいてそれを変更して返却するような場合ではコピー代入演算子のコスト分お得です。私の解答はここに主眼をおいていました。

mutableにできる場合はChironianさんの解答が該当ですね。

std::moveするのって

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

ex.) std::vector::emplace_back

f(std::move(a));

この時関数の引数の型は

void f1(C c);
void f2(C&& c);//rvalue reference
template<typename CC>
void f3(CC&& c);//universal reference(forwarding reference)

でないとmove semanticsできない(=所有権の放棄を識別できない)

f1の場合は関数呼び出し時点でmove ctor呼び出し。内部で再度moveするなら避けるべき

f2の場合は関数の内部でmove semanticsできる。

f3はどちらかというとparfect forwarding

2) move ctorを呼び出す時

C c2 = std::move(c1);

が大半な気がします。

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/04 15:13 編集

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

    yumetodoさん指摘の通り、2019年現在のC++コンパイラはこのmove関数呼び出しが冗長かつ有害と警告するようになっています。

    キャンセル

  • 2019/05/04 22: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がややこしい)

    ちょっと私が混乱していてまだ理解が整理できていないため、少し時間をください。
    (完全転送など、ちょっとまだ理解てきていない)
    ご回答有り難うございました。

    キャンセル

+2

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

正しい認識と思います。

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

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

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/04 22:29

    まさか勉強していたブログのご本人様から回答を頂けるとは思ってませんでした汗

    >このような関数の性質(参照透過性)はプログラムの可読性や保守性といった観点から好ましいスタイルとされています。

    あまりちゃんと理解できていませんが、関数型言語のように破壊的変更を許さないように
    したほうが安全だから取り込んでいこう、というトレンドがあるのは感じております。

    >実行時効率まで考慮して最適なものを選ぼうとすると少々厄介です

    私自身が個人的に実現したいことは、今仕事でよく扱うデータがグリッド地図などを表現する
    2次元vectorを扱うことが多いため、
    - vectorの愚直な値コピーは発生させない
    - それさえ守れれば後は可読性が高く、バグが出づらいコードを書きたい
    になっています。なので、微差な計算コストは許容できる、
    それより分かりやすさを優先したいと考えています。

    ご紹介いただいたCore Guidelines、チラ見した限り更に混乱しましたが、(笑
    もう少し頑張ってみます・・・!

    キャンセル

  • 2019/05/04 23:01

    とりあえず網羅的に載っているので C++ Core Guidelines を紹介しはしましたが、内容更新のアクティビティがやたら高い(≒安定してない)のと、無邪気に最新規格(策定前のC++20等)を前提にしていたりと、現実の業務利用においてはあくまで参考程度で見てください。

    ムーブセマンティクスはあくまでも「関数入出力設計の選択肢が増えた」にすぎませんから、既存コードベースとの整合性やチームメンバの練度と相談しながら導入していくのが現実解とは思います。

    > まさか勉強していたブログ
    多少なりともお役に立っていればなによりです :D

    キャンセル

+1

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


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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/05 20:18

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

    キャンセル

  • 2019/05/06 18:00

    ありがとうございます、確かにreturnでmoveする場合としない場合で顕著に違いました。
    本業では左辺値参照の出力用仮引数を使う書き方でしばらくは続けたいと思います。
    組み込み用のレガシーな世界にいるので、移植性やメンバの練度の観点で、RVO/NRVOを前提とした実装を職場でacceptしてもらえるかは検討が必要そうです。が、個人の趣味プログラミングでは使っていきたいと思います。

    色々な方から有益な情報をもらいまして、正直ベストアンサーを決められない状態になってしまいました。
    ただ、自分のレベルに適した回答とNRVOのテスト例をしていただけたasmさんにこの場ではさせていただきたいと思います。
    みなさんのTwitterやQiitaの記事も参考になることが多く、今も大変助かっています。ありがとうございました。

    キャンセル

checkベストアンサー

0

参照が適さないケースとして
圧縮されたデータを展開するシナリオを考えます。

std::vector<byte> inflate1(const std::vector<byte>& zipped){
  size_t len = *(size_t*)zipped.data();
  std::vector<byte> result(len);

  // なんらかのデータ展開処理

  return result;
}

void inflate2(const std::vector<byte> &zipped, std::vector<byte> &result){
  size_t len = *(size_t*)zipped.data();
  result.swap(std::vecor<byte>(len));

  // なんらかのデータ展開処理
}

int main(){
  std::vector<byte> zip{100,0,0,0,0xff,0xff};
  {
    std::vector<byte> result = inflate1(zip);
  }
  {
    std::vector<byte> result;
    inflate2(zip, result);
  }
}

この場合、NRVOを用いたinflate1の方がわかりやすく、効率もよいでしょう。
inflate2は、呼ばれた時点でresultに何が入っているのか・どういう状況なのかがわかりません。
そのため、解放および初期化が必要になります。


上ではムーブセマンティクスを使いませんでした。
というのも、現状のムーブセマンティクスには欠陥があり必要でないなら使わない方がパフォーマンス上よい
と私は思っているからです。
その欠陥は、ムーブした後の残骸であろうとデストラクタが呼び出される事です。

逆に使う必要がある時というのは限られています。
それは、

  • コピー代入が使えないとき
  • 参照が使えないとき

です。

思いつくのは・・・

#include <iostream>
#include <memory>
#include <cstdlib>

int main(){
    using namespace std;
    unique_ptr<char> t = make_unique<char>('Q');
    for(int i=38;i<100;i++){
        unique_ptr<char> s = make_unique<char>(i);

        // 適当な条件式
        if(rand() % 100 > 93){
            t = std::move(s);
            break;
        }
    }
    if(t) cout << *t << endl;
}

こんな感じでしょうか

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/07 10:58

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

    キャンセル

  • 2019/05/07 23:16

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

    キャンセル

  • 2019/05/08 12:12

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

    キャンセル

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.を回避できます。

std::vector<int> twice_vec(std::vector<int>&& vec) {
  for (auto& e : vec) {
    e *= 2;
  }

  return std::move(vec);
}


このケースでは、returnのstd::moveは書いた方がいいような気がします。
NRVOは機能できない筈ですし、構文的にはコピーになる筈です。コンパイラがstd::moveなしでも左辺値をムーブしてくれればよいのですが。

↓右辺値参照は左辺値です。わけわからんですね。
https://cpprefjp.github.io/lang/cpp11/rvalue_ref_and_move_semantics.html

右辺値参照で宣言された変数は右辺値ではなく、左辺値である。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/04 17:27

    >更に、std::moveの必要性を理解せずにstd::vector<int> b = twice_vec(a);と書かれると泣きたくなるかも。

    一ミリもmoveが必要ないんですがそれは

    キャンセル

  • 2019/05/04 17:30

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

    キャンセル

  • 2019/05/04 17:33 編集

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

    キャンセル

  • 2019/05/04 22:36

    >std::moveの必要性を理解せずにstd::vector<int> b = twice_vec(a);と書かれると泣きたくなるかも。

    おっしゃるように、今の私のチームでムーブセマンティクスを使ったコードを書いたら
    「なんで値渡しする関数書いてるの!?」とマサカリの的になると思います。
    チームでコンセンサスを取って使わないと混乱しそうです。
    一応、&&で関数の引数を右辺地参照に明示的に限定することもできるのですね。

    まだ理解が浅いので、仕事で使うのは控えておくようにします。
    ただ、理解はできるようがんばります。

    キャンセル

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

  • ただいまの回答率 89.07%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る