web-dev-qa-db-ja.com

const参照のコストが1つのコピーのみである場合、C ++ 11では値渡し(コピーが必要な場合)が推奨されるのはなぜですか?

移動のセマンティクス、右辺値参照、std::moveなどを理解しようとしています。このサイトでさまざまな質問を検索して、const std::string &name + _name(name)を渡すのがstd::string name +よりも推奨されない理由を見つけようとしています_name(std::move(name))コピーが必要な場合。

私が正しく理解している場合、次の場合、単一のコピー(コンストラクターを使用)と移動(一時からメンバーへ)が必要です。

Dog::Dog(std::string name) : _name(std::move(name)) {}

別の(かつ昔ながらの)方法は、参照で渡し、コピー(参照からメンバーへ)することです。

Dog::Dog(const std::string &name) : _name(name) {}

最初の方法がコピーと移動の両方を必要とし、2番目の方法が1つのコピーのみを必要とする場合、最初の方法を優先し、場合によってはより高速にする方法を教えてください。

31
John Bonata

consuming dataの場合、消費できるオブジェクトが必要です。 std::string const& you will引数が必要かどうかに関係なくオブジェクトをコピーする必要があります。

オブジェクトが値で渡される場合、オブジェクトをコピーする必要がある場合、つまり、渡されたオブジェクトが一時的でない場合、オブジェクトはコピーされます。ただし、それが一時的なものである場合、オブジェクトは所定の位置に構築される可能性があります。つまり、コピーが省略され、移動構築の費用が発生するだけです。つまり、実際にコピーが発生しない可能性があります。

30
Dietmar Kühl

左辺値と右辺値でさまざまなオプションを呼び出すことを検討してください。

  1. Dog::Dog(const std::string &name) : _name(name) {}
    

    左辺値または右辺値のどちらで呼び出されようと、nameから_nameを初期化するには、コピーが1つだけ必要です。 nameconstであるため、移動はオプションではありません。

  2. Dog::Dog(std::string &&name) : _name(std::move(name)) {}
    

    これは右辺値でのみ呼び出すことができ、移動します。

  3.  Dog::Dog(std::string name) : _name(std::move(name)) {}
    

    左辺値で呼び出された場合、これはコピーして引数を渡し、次に移動してデータメンバーを設定します。右辺値で呼び出された場合、これは引数を渡すために移動し、次にデータメンバーを設定するために移動します。右辺値の場合、引数を渡すための移動は省略できます。したがって、これを左辺値で呼び出すと1回のコピーと1回の移動が行われ、右辺値でこれを呼び出すと1〜2回の移動が行われます。

最適な解決策は、(1)(2)の両方を定義することです。解決策(3)は、最適と比較して余分な動きを持つことができます。ただし、1つの関数を記述する方が、実質的に同じ2つの関数を記述するよりも短く、保守しやすく、移動は安価であると想定されます。

const char*のような文字列に暗黙的に変換可能な値で呼び出すと、長さの計算と文字列データのコピーを伴う暗黙的な変換が行われます。次に、右辺値のケースに分類されます。この場合、string_viewを使用すると、さらに別のオプションが提供されます。

  1. Dog::Dog(std::string_view name) : _name(name) {}
    

    文字列左辺値または右辺値で呼び出されると、これは1つのコピーになります。 const char*で呼び出された場合、1つの長さの計算と1つのコピーが行われます。

26
Jeff Garrett

最初の短い答え:const&による呼び出しは常にコピーに費用がかかります。条件に応じて値による呼び出しは1回の移動にかかる場合があります。ただし、状況によって異なります(この表が参照するシナリオについては、以下のコード例をご覧ください)。

            lvalue        rvalue      unused lvalue  unused rvalue
            ------------------------------------------------------
const&      copy          copy        -              -
rvalue&&    -             move        -              -
value       copy, move    move        copy           - 
T&&         copy          move        -              -
overload    copy          move        -              - 

したがって、私のエグゼクティブサマリーは、値による呼び出しは、次の場合に考慮する価値があるということです。

  • 余分な動きがあるかもしれないので、動きは安いです
  • パラメーターは無条件に使用されます。値による呼び出しは、パラメーターが使用されていない場合、コピーにも費用がかかります。 if句またはsthのため。

値で呼び出す

引数をコピーするために使用される関数を考えます

class Dog {
public:
    void name_it(const std::string& newName) { names.Push_back(newName); }
private:
    std::vector<std::string> names;
};

name_itに左辺値が渡される場合、右辺値の場合にも2つのコピー操作があります。右辺値が私を動かすことができるので、それは悪いです。

考えられる1つの解決策は、右辺値のオーバーロードを記述することです。

class Dog {
public:
    void name_it(const std::string& newName) { names.Push_back(newName); }
    void name_it(std::string&& newName) { names.Push_back(std::move(newName)); }
private:
    std::vector<std::string> names;
};

これで問題は解決し、まったく同じコードを持つ2つのコード2つの関数があるにもかかわらず、すべてが問題ありません。

別の実行可能なソリューションは完全な転送を使用することですが、それにはいくつかの欠点もあります(たとえば、完全な転送機能は非常に貪欲で、既存のオーバーロードされたconst&関数を役に立たないものにします。通常、ヘッダーファイルにある必要があります) 、それらはオブジェクトコードなどにいくつかの関数を作成します。)

class Dog {
public:
    template<typename T>
    void name_it(T&& in_name) { names.Push_back(std::forward<T>(in_name)); }
private:
    std::vector<std::string> names;
};

さらに別の解決策値による呼び出しを使用することです。

class Dog {
public:
    void name_it(std::string newName) { names.Push_back(std::move(newName)); }
private:
    std::vector<std::string> names;
};

あなたがstd::moveに言及したように、重要なことです。このようにして、右辺値と左辺値の両方に対して1つの関数を使用できます。右辺値を移動しますが、左辺値の追加の移動を受け入れます。これは移動が安価な場合で、条件に関係なくパラメーターをコピーまたは移動できます。

だから、最後に、ある方法を他の方法より推奨するのは明らかに間違っていると思います。それは強く依存します。

#include <vector>
#include <iostream>
#include <utility>

using std::cout;

class foo{
public:
    //constructor
    foo()  {}
    foo(const foo&)  { cout << "\tcopy\n" ; }
    foo(foo&&)  { cout << "\tmove\n" ; }
};

class VDog {
public:
    VDog(foo name) : _name(std::move(name)) {}
private:
    foo _name;
};

class RRDog {
public:
    RRDog(foo&& name) : _name(std::move(name)) {}
private:
    foo _name;
};

class CRDog {
public:
    CRDog(const foo& name) : _name(name) {}
private:
    foo _name;
};

class PFDog {
public:
    template <typename T>
    PFDog(T&& name) : _name(std::forward<T>(name)) {}
private:
    foo _name;
};

//
volatile int s=0;

class Dog {
public:
    void name_it_cr(const foo& in_name) { names.Push_back(in_name); }
    void name_it_rr(foo&& in_name)   { names.Push_back(std::move(in_name));}

    void name_it_v(foo in_name) { names.Push_back(std::move(in_name)); }
    template<typename T>
    void name_it_ur(T&& in_name) { names.Push_back(std::forward<T>(in_name)); }
private:
    std::vector<foo> names;
};


int main()
{
    std::cout << "--- const& ---\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue:";
        a.name_it_cr(my_foo);
        std::cout << "rvalue:";
        b.name_it_cr(foo());
    }
    std::cout << "--- rvalue&& ---\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue: -\n";
        std::cout << "rvalue:";
        a.name_it_rr(foo());
    }
    std::cout << "--- value ---\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue:";
        a.name_it_v(my_foo);
        std::cout << "rvalue:";
        b.name_it_v(foo());
    }
    std::cout << "--- T&&--\n";
    {
        Dog a,b;
        foo my_foo;
        std::cout << "lvalue:";
        a.name_it_ur(my_foo);
        std::cout << "rvalue:";
        b.name_it_ur(foo());
    }


    return 0;
}

出力:

--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
    move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move
5
DrSvanHay

私は実験をしました:

#include <cstdio>
#include <utility>

struct Base {
  Base() { id++; }
  static int id;
};

int Base::id = 0;

struct Copyable : public Base {
  Copyable() = default;
  Copyable(const Copyable &c) { printf("Copyable [%d] is copied\n", id); }
};

struct Movable : public Base {
  Movable() = default;

  Movable(Movable &&m) { printf("Movable [%d] is moved\n", id); }
};

struct CopyableAndMovable : public Base {
  CopyableAndMovable() = default;

  CopyableAndMovable(const CopyableAndMovable &c) {
    printf("CopyableAndMovable [%d] is copied\n", id);
  }

  CopyableAndMovable(CopyableAndMovable &&m) {
    printf("CopyableAndMovable [%d] is moved\n", id);
  }
};

struct TEST1 {
  TEST1() = default;
  TEST1(Copyable c) : q(std::move(c)) {}
  TEST1(Movable c) : w(std::move(c)) {}
  TEST1(CopyableAndMovable c) : e(std::move(c)) {}

  Copyable q;
  Movable w;
  CopyableAndMovable e;
};

struct TEST2 {
  TEST2() = default;
  TEST2(Copyable const &c) : q(c) {}
  //  TEST2(Movable const &c) : w(c)) {}
  TEST2(CopyableAndMovable const &c) : e(std::move(c)) {}

  Copyable q;
  Movable w;
  CopyableAndMovable e;
};

int main() {
  Copyable c1;
  Movable c2;
  CopyableAndMovable c3;
  printf("1\n");
  TEST1 z(c1);
  printf("2\n");
  TEST1 x(std::move(c2));
  printf("3\n");
  TEST1 y(c3);

  printf("4\n");
  TEST2 a(c1);
  printf("5\n");
  TEST2 s(c3);

  printf("DONE\n");
  return 0;
}

結果は次のとおりです。

1
Copyable [4] is copied
Copyable [5] is copied
2
Movable [8] is moved
Movable [10] is moved
3
CopyableAndMovable [12] is copied
CopyableAndMovable [15] is moved
4
Copyable [16] is copied
5
CopyableAndMovable [21] is copied
DONE

結論:

template <typename T>
Dog::Dog(const T &name) : _name(name) {} 
// if T is only copyable, then it will be copied once
// if T is only movable, it results in compilation error (conclusion: define separate move constructor)
// if T is both copyable and movable, it results in one copy

template <typename T>
Dog::Dog(T name) : _name(std::move(name)) {}
// if T is only copyable, then it results in 2 copies
// if T is only movable, and you called Dog(std::move(name)), it results in 2 moves
// if T is both copyable and movable, it results in one copy, then one move.
0
warchantua

パフォーマンス上の理由以外で、コピーが値ごとのコンストラクターで例外をスローする場合、コンストラクター内ではなく、最初に呼び出し側でスローされます。これにより、noexceptコンストラクターをコーディングしやすくなり、リソースリークやコンストラクターのtry/catchブロックを心配する必要がなくなります。

struct A {
    std::string a;

    A( ) = default;
    ~A( ) = default;
    A( A && ) noexcept = default;
    A &operator=( A && ) noexcept = default;

    A( A const &other ) : a{other.a} {
        throw 1;
    }
    A &operator=( A const &rhs ) {
        if( this != &rhs ) {
            a = rhs.a;
            throw 1;
        }
        return *this;
    }
};

struct B {
    A a;

    B( A value ) try : a { std::move( value ) }
    { std::cout << "B constructor\n"; }
    catch( ... ) {
        std::cerr << "Exception in B initializer\n";
    }
};

struct C {
    A a;

    C( A const &value ) try : a { value }
    { std::cout << "C constructor\n"; }
    catch( ... ) {
        std::cerr << "Exception in C initializer\n";
    }
};

    int main( int, char ** ) {

    try {
        A a;
        B b{a};
    } catch(...) { std::cerr << "Exception outside B2\n"; }



    try {
        A a;
        C c{a};
    } catch(...) { std::cerr << "Exception outside C\n"; }

    return EXIT_SUCCESS;
}

出力します

Exception outside B2
Exception in C initializer
Exception outside C
0
Beached