7

最近、コンパイル エラーが発生したため、以下のコードに関して SE に質問を投稿しました。ムーブ コンストラクターまたはムーブ代入演算子を実装すると、デフォルトのコピー コンストラクターが削除されると、誰かが親切に答えてくれました。彼らはまた、次のstd::move()ようなものを機能させるために を使用する必要があることを提案しました。

Image src(200, 200);
Image cpy = std::move(src);

この場合、移動代入演算子または移動コンストラクターを使用するという事実を明示する必要があるため、これは私には理にかなっています。この例では左辺値であり、これを で明示的に表現しない限りsrc、実際にコンテンツを移動したいことをコンパイラに伝えることはできません。ただし、このコードにはさらに問題があります。cpystd::move

Image cpy = src + src

以下のコピーは入れませんでしたが、operator +次のタイプの単純なオーバーロード演算子です。

Image operator + (const Image &img) const {
    Image tmp(std::min(w, img.w), std::min(h, img.h));
    for (int j = 0; j < tmp.h; ++j) {
        for (int i = 0; i < tmp.w; ++i) {
            // accumulate the result of the two images
        }
    }
    return tmp; 
}

この特定のケースでは、演算子が の形式で一時変数を返し、tmpその場合に に到達すると移動代入演算子がトリガーされると想定しますcpy = src + srcsrc + srcの結果が左辺値であると言うのが正確かどうかはわかりません。実際には に返されるものtmpが にtmpコピー/代入されるためcpyです。したがって、移動演算子が存在する前は、これによりデフォルトのコピー コンストラクターがトリガーされていました。しかし、この場合、移動コンストラクターを使用しないのはなぜでしょうか? 私もする必要があるようです:

Image cpy = std::move(src + src);

operator +これを機能させるには、クラス Imageによって返される変数の xvalue を取得すると仮定しますか?

誰かがこれをよりよく理解するのを手伝ってくれませんか? 私が正しくないことを教えてください。

ありがとうございました。

#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <fstream>
#include <cassert>

class Image
{
public:
    Image() : w(512), h(512), d(NULL)
    {
        //printf("constructor default\n");
        d = new float[w * h * 3];
        memset(d, 0x0, sizeof(float) * w * h * 3);
    }
    Image(const unsigned int &_w, const unsigned int &_h) : w(_w), h(_h), d(NULL)
    {
        d = new float[w * h * 3];
        memset(d, 0x0, sizeof(float) * w * h * 3);
    }
    // move constructor
    Image(Image &&img) : w(0), h(0), d(NULL)
    {
        w = img.w;
        h = img.h;
        d = img.d;
        img.d = NULL;
        img.w = img.h = 0;
    }
    // move assignment operator
    Image& operator = (Image &&img)
    {
        if (this != &img) {
            if (d != NULL) delete [] d;
            w = img.w, h = img.h;
            d = img.d;
            img.d = NULL;
            img.w = img.h = 0;
        }
        return *this;
    }
    //~Image() { if (d != NULL) delete [] d; }
    unsigned int w, h;
    float *d;
};

int main(int argc, char **argv)
{
    Image sample;// = readPPM("./lean.ppm");
    Image res = sample;
    return 0;
}
4

1 に答える 1

6

私もする必要があるようです:

Image cpy = std::move(src + src);

あなたの場合ではありません。の

Image operator + (const Image &img) const {
    Image tmp;
    // ...
    return tmp; 
}

関数の戻り値の型と同じ型のオブジェクトを作成して返しています。これは、12.8/32 の右辺値であるかのようにreturn tmp;見なされることを意味します (強調鉱山)tmp

コピー操作の省略の基準が満たされているか、ソース オブジェクトが関数パラメーターであり、コピーされるオブジェクトが左辺値によって指定されているという事実を除いて満たされる場合、コピーのコンストラクターを選択するためのオーバーロードの解決は次のとおりです。オブジェクトが右辺値によって指定されたかのように最初に実行されます。

前述の基準は 12.8/31 に記載されています。特に、最初の箇条書きには次のように記載されています (強調は私のものです)。

—クラスの戻り値の型を持つ関数の return ステートメントで、式が関数の戻り値の型と同じ cv-unqualified 型を持つ不揮発性自動オブジェクト(関数または catch-clause パラメーター以外)の名前である場合、自動オブジェクトを関数の戻り値に直接構築することにより、コピー/移動操作を省略できます

実際、12.8/31 を注意深く読むと、あなたの場合、コンパイラはコピーを省略したり、完全に移動したりすることが許可されています (最も一般的なものは許可されています)。これは、いわゆる戻り値の最適化(RVO) です。実際、この単純化されたバージョンのコードを検討してください。

#include <cstdlib>
#include <iostream>

struct Image {

    Image() {
    }

    Image(const Image&) {
        std::cout << "copy\n";
    }

    Image(Image&&) {
        std::cout << "move\n";
    }

    Image operator +(const Image&) const {
        Image tmp;
        return tmp;
    }
};

int main() {
    Image src;
    Image copy = src + src;
}

GCC 4.8.1 でコンパイルされたこのコードは出力を生成しません。つまり、移動操作のコピーは実行されません。

RVO を実行できない場合に何が起こるかを確認するために、コードを少し複雑にしてみましょう。

    Image operator +(const Image&) const {
        Image tmp1, tmp2;
        if (std::rand() % 2)
            return tmp1;
        return tmp2;
    }

多くの詳細がなければ、ここで RVO を適用することはできません。これは、標準で禁止されているからではなく、他の技術的な理由からです。operator +()コード出力のこの実装でmove。つまり、コピーはなく、移動操作のみです。

最後に、OP の zoska に対する Matthieu M の応答に基づいています。Matthieu M が正しく言っreturn std::move(tmp);たように、RVO を防ぐため、実行することはお勧めできません。確かに、この実装では

    Image operator +(const Image&) const {
        Image tmp;
        return std::move(tmp);
    }

出力は ですmove。つまり、move コンストラクターが呼び出されますが、これまで見てきたように、return tmp;copy/move コンストラクターは呼び出されません。これは正しい動作です。これは、上記の RVO ルールで要求されるように、戻り値である式が不揮発性の自動オブジェクトの名前でstd::move(tmp)ないためです。

更新user18490 コメントに応じて。これの実装は、 operator +()RVOtmptmp2防止するためのむしろ人為的な方法です。最初の実装に戻って、RVO を防止する別の方法を考えてみましょう。これも全体像を示しています。オプションを使用してコードをコンパイルします-fno-elide-constructors(clang でも使用可能)。出力 (GCC では、clang では異なる場合があります) は次のとおりです。

move
move

関数が呼び出されると、返されるオブジェクトを構築するためにスタック メモリが割り当てられます。これは上記の変数ではないことを強調しますtmp。これは別の名前のない一時オブジェクトです。

次に、return tmp;から名前のないオブジェクトへのコピーまたは移動をトリガーしtmp、初期化によって名前のないオブジェクトをImage cpy = src + src;にコピー/移動しますcpy。それが基本的なセマンティクスです。

最初のコピー/移動に関しては、次のとおりです。は左辺値であるためtmp、通常はコピー コンストラクターを使用しtmpて名前のないオブジェクトからコピーします。ただし、上記の特別な節は例外であり、in は右辺値であるかのように見なされるべきであると述べていtmpますreturn tmp;。したがって、move コンストラクターが呼び出されます。さらに、RVO が実行されると、移動は省略さtmpれ、名前のないオブジェクトの上に実際に作成されます。

2 番目のコピー/移動に関しては、さらに簡単です。名前のないオブジェクトは右辺値であるため、そこから に移動するために移動コンストラクターが選択されcpyます。現在、別の最適化 (RVO に似ていますが、AFAIK には名前がありません) も 12.8/31 (3 番目の箇条書き) に記載されており、コンパイラが名前のない一時的な使用を回避し、cpy代わりにメモリを使用できるようにします。したがって、RVO とこの最適化が行われているtmp場合、名前のないオブジェクトとcpyは本質的に「同じオブジェクト」になります。

于 2013-10-18T08:54:22.580 に答える