92431


c++でクラス型変数を返却した際の処理の流れについて質問をさせていただきたいです。

例えば

[code]

    class Hoge{
    //クラス実装
    };
    
    Hoge func(){
      Hoge fuga;
    
      // fugaの作成
    
      return std::move(fuga);
    }
    
    int main(){
      Hoge foo = func();
    
      return 0;
    }
    
[/code]

とした場合、func終了時の処理の流れとしては

  1. fugaを使用したムーブコンスラクトによってfuncの戻り値が構築される
  2. funcの戻り値は右辺値なので、fooはfuncの戻り値によってムーブコンストラクトされる

というもので合っているのでしょうか？  
疑問に思っているのは

  1. fugaは左辺値なので、funcの戻り値をfugaからムーブコンストラクトするには明示的にstd::moveする必要があると思っていますが、これは合っているのでしょうか？
  2. 関数の戻り値によって変数の初期化が行われる場合、ムーブコンストラクトが行われるのでしょうか、それともNRVOなどでコンストラクタが省略されるのでしょうか？  
という部分となります。

よろしくお願いいたします。

【追記】  
使用するC++のバージョンはC++14を想定していますが、  
もしC++17や20で挙動が変わるようでしたら、  
それを含めて教えていただけますと幸いです。


CC BY-SA 4.0
2022-11-25T04:39:45.450
92421
2022-11-25T13:53:38.137
2022-11-25T04:57:50.523
55583
55583
question
c++
クラス型変数をreturnした際の流れにつきまして


提示コードは NRVO ができなくなる典型例ですよね

[code]

    #include <utility>
    class Hoge {};
    Hoge func1() {
        Hoge h;
        return h;
    }
    Hoge func2() {
        Hoge h;
        return std::move(h);
    }
    
[/code]

というコードに対して g++ / clang++ は

[code]

    $ g++ -std=c++14 -W -Wall -pedantic -O -g -c nrvo.cpp 
    nrvo.cpp: In function 'Hoge func2()':
    nrvo.cpp:9:21: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
        9 |     return std::move(h);
          |            ~~~~~~~~~^~~
    nrvo.cpp:9:21: note: remove 'std::move' call
    $ clang++ -std=c++14 -W -Wall -pedantic -O -g -c nrvo.cpp 
    nrvo.cpp:9:12: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
        return std::move(h);
               ^
    nrvo.cpp:9:12: note: remove std::move call here
        return std::move(h);
               ^~~~~~~~~~ ~
    1 warning generated.
    $
    
[/code]

のように文句を言いますので、この例では `std::move` を書かないほうが良いと判断されているようです。

[code]

    $ g++ --version
    g++ (GCC) 11.3.0
    Copyright (C) 2021 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.  There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    
    $ clang++ --version
    clang version 8.0.1 (tags/RELEASE_801/final)
    Target: x86_64-unknown-windows-cygnus
    Thread model: posix
    InstalledDir: /usr/bin
    $ uname -a
    CYGWIN_NT-10.0-19044 mypc 3.3.6-341.x86_64 2022-09-05 11:15 UTC x86_64 Cygwin
    $
    
[/code]

ちなみに警告は `-std=c++17` でも表示されました。

RVO/NRVO はほとんどのコンパイラで C++98 の頃から実装されてました（言語規格書的には義務付けられていないけど）ので、下位互換性まで考えて
`std::move` なしで書くべきでしょう。


CC BY-SA 4.0
2022-11-25T07:06:28.333
92423
2022-11-25T07:06:28.333
8589
92421
answer


>   * `fuga` は左辺値なので、 `func` の戻り値を `fuga` からムーブコンストラクトするには明示的に `std::move`
> する必要があると思っていますが、これは合っているのでしょうか？
>   *
> 関数の戻り値によって変数の初期化が行われる場合、ムーブコンストラクトが行われるのでしょうか、それともNRVOなどでコンストラクタが省略されるのでしょうか？
>

誤っています。

[戻り値の型が参照ではない関数を呼び出した結果の型は prvalue](https://timsong-
cpp.github.io/cppwp/n3337/basic.lval#1.5)です。 `return`
式に与えられている式が右辺値か左辺値かに関係なく。

`prvalue` は右辺値の一種とみなされ `foo` の初期化子として現れた場合にはムーブが発生しますが「`prvalue`
で初期化するときはコピーを省略してもかまわない (C++17
以降は省略しなければならない)」という規則が適用され、またそのときにはコピーもムーブもされません。 直接初期化されたのと同じであるとみなされます。

`return` に与える式について `std::move` を使った場合には一時オブジェクトの生成に対してムーブを強制することになってしまいます。
コピーであれば省略できるコピーと解釈されるところをムーブを強制すればムーブは確実にされます。

素朴な解釈だと「`return`
に与えた式」→「変数が返す一時オブジェクト(変数の初期化子)」→「コンストラクタに渡される値」という段階があり各所でコピーが発生してしまうので色々と条件を付けて省略できるようになっているわけです。
今回の質問の例では `return` 式で `std::move`
をしようがしまいが関数の返却値から変数の初期化ではコピーもムーブも省略できる条件が揃うわけですが、一時オブジェクトの生成について `std::move` が
(ムーブの強制になるので) コピーの省略を阻害するという形になります。


CC BY-SA 4.0
2022-11-25T09:09:40.517
92425
2022-11-25T09:09:40.517
3364
92421
answer


# 回答ではありません

回答者に依頼しても出てこないので、簡単に出来る範囲で調べてみました(別途質問もできないので)。  
簡単な検証結果を置いておきます。

[code]

    #include <iostream>
    using namespace std;
    struct Hoge{
        Hoge() {cout << "Hoge()" << endl;}
        Hoge(const Hoge& org) {cout << "Hoge(const Hoge&)" << endl;}
        Hoge& operator=(const Hoge& org) {cout << "operator=(const Hoge&)" << endl;return *this;}
        ~Hoge() {cout << "~Hoge()" << endl;}
    #if (__cplusplus >= 201103L)
        Hoge(Hoge&& org) {cout << "Hoge(Hoge&&)" << endl;}
        Hoge& operator=(Hoge&& org) {cout << "operator=(Hoge&&)" << endl;return *this;}
    #endif
    };
    Hoge func(){
      Hoge fuga;
      return fuga;
    }
    int main(){
      Hoge foo1 = func();
      Hoge foo2 = foo1;
      foo2 = foo1;
    #if (__cplusplus >= 201103L)
      Hoge foo3 = std::move(foo1);
      foo3 = std::move(foo2);
    #endif
      return 0;
    }
    
[/code]

これを以下のサイトで実行してみました。  
<https://godbolt.org/>  
対象は以下の処理系です。  
clang x86-64 3.3, 3.4.1, 3.5.2, 3.6, 3.7, 3.8.1, 3.9.1, 4.0.1, 5.0.2, 6.0.1,
7.0.1, 7.1.0, 8.0.1, 9.0.1, 10.0.1, 11.0.1, 12.0.1, 13.0.1, 14.0.0, 15.0.0  
3.3, 3.4.1, 3.5.2, 3.6, 3.7, 3.8.1, 3.9.1, 4.0.1, 5.0.2, 6.0.1, 7.0.1, 7.1.0,
8.0.1, 9.0.1, 10.0.1, 11.0.1, 12.0.1, 13.0.1, 14.0.0, 15.0.0  
gcc x86-64 4.7.4, 4.8.5, 4.9.3, 5.5, 6.4, 7.5, 8.5, 9,5, 10.4, 11.3, 12.2  
msvc x64 19.33(これのみ実施)  
(末尾のバージョンだけ違う場合は最大のもの以外は未実施)

確認項目1  
-Wall -std=c++98で以下の出力であること。(msvcは未実施)
[code]

    Hoge()
    Hoge(const Hoge&)
    operator=(const Hoge&)
    ~Hoge()
    ~Hoge()
    
[/code]

確認項目2  
-Wall -std=c++XX(XXは11,14,17,2a,20,2b,23でコンパイル可能な最大値)で以下の出力であること。
[code]

    Hoge()
    Hoge(const Hoge&)
    operator=(const Hoge&)
    Hoge(Hoge&&)
    operator=(Hoge&&)
    ~Hoge()
    ~Hoge()
    ~Hoge()
    
[/code]

(msvcは/std:c++20 /Zc:__cplusplus /O2で実施。/O2を外して最適化を切るとcopyが入る。)  
※確認は目視(アセンブラは見てない)です。こういうテストが出来るオンラインの環境があると楽なんですが…。

結果  
clang/gccについては記載したバージョン全てでコピー省略が最適化なしで確認できた。msvcでは最適化時のみ確認できた。

追記  
msvcで最適化しないときの出力ではmove constructorになってました。

[code]

    Hoge()
    Hoge(Hoge&&)
    ~Hoge()
    Hoge(const Hoge&)
    operator=(const Hoge&)
    Hoge(Hoge&&)
    operator=(Hoge&&)
    ~Hoge()
    ~Hoge()
    ~Hoge()
    
[/code]


CC BY-SA 4.0
2022-11-25T12:00:27.817
92429
2022-11-25T13:39:03.633
2022-11-25T13:39:03.633
54957
54957
92421
answer


> func終了時の処理の流れとしては
>
>   1. fugaを使用したムーブコンスラクトによってfuncの戻り値が構築される
>   2. funcの戻り値は右辺値なので、fooはfuncの戻り値によってムーブコンストラクトされる
>

>
> というもので合っているのでしょうか？

いずれも部分的には正しいです。厳密な挙動は下記の通りです。

  1. `std::move`関数の明示利用により、常にムーブコンストラクトが行われます。 
    * このケースでは`std::move`関数を利用しない、つまり`return fuga;`という記述が好ましいです。最悪でも暗黙のムーブによるムーブコンストラクトが保証されます。
    * C++コンパイラの最適化によってはNRVO(Named Return Value Optimization)が行われ、ムーブコンストラクタ呼び出しが省略される可能性があります。
  2. C++11〜C++14までと、C++17以降で挙動が異なります。 
    * C++11〜C++14：最悪でもムーブコンストラクトが行われます（コピーコンストラクトではなく）。C++コンパイラの最適化によってはRVO(Return Value Optimization)が行われ、ムーブコンストラクタ呼び出しが省略される可能性があります。
    * C++17以降：常にRVO(Return Value Optimization)が行われると保証されます。

* * *

>
> fugaは左辺値なので、funcの戻り値をfugaからムーブコンストラクトするには明示的にstd::moveする必要があると思っていますが、これは合っているのでしょうか？

いいえ。

ローカル変数の名前（左辺値）をreturn文に指定する場合、まずは右辺値であるかのように読み替えられてムーブコンストラクトが試行されます。この試行がコンパイルエラーとなるようであれば、元の左辺値から改めてコピーコンストラクトが試行されます。

>
> 関数の戻り値によって変数の初期化が行われる場合、ムーブコンストラクトが行われるのでしょうか、それともNRVOなどでコンストラクタが省略されるのでしょうか？

上記回答の通りです。なお、ここでの最適化は"NRVO"ではなく"RVO"です。


CC BY-SA 4.0
2022-11-25T13:13:46.363
92431
2022-11-25T13:53:38.137
2022-11-25T13:53:38.137
49
49
92421
answer