Подтвердить что ты не робот

Почему передача по значению (если требуется копия), рекомендуемая в С++ 11, если ссылка на константу стоит только на одну копию?

Я пытаюсь понять семантику перемещения, rvalue-ссылки, std::move и т.д. Я пытался выяснить, просматривая различные вопросы на этом сайте, почему менее рекомендуется использовать const std::string &name + _name(name) чем a std::string name + _name(std::move(name)), если требуется копия.

Если я правильно понимаю, для следующего требуется одиночная копия (через конструктор) плюс перемещение (от временного к члену):

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

Альтернативный (и старомодный) способ - передать его по ссылке и скопировать его (из ссылки на участника):

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

Если для первого метода требуется копирование и перемещение обоих, а для второго метода требуется только одна копия, как первый метод может быть предпочтительным, а в некоторых случаях быстрее?

4b9b3361

Ответ 1

При использовании данных вам понадобится объект, который вы можете использовать. Когда вы получите std::string const&, вам придется скопировать объект независимо от того, нужен ли аргумент.

Когда объект передается по значению, объект будет скопирован, если он должен быть скопирован, т.е. когда переданный объект не является временным. Однако, если это временно, объект может быть построен на месте, т.е. Любые копии могут быть удалены, и вы просто платите за строительство движения. То есть, существует вероятность того, что копия на самом деле не произойдет.

Ответ 2

Рассмотрим возможность вызова различных параметров с lvalue и с rvalue:

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

    Вызывается ли с lvalue или rvalue, для этого требуется ровно одна копия, чтобы инициализировать _name из name. Перемещение не является вариантом, потому что name - const.

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

    Это можно вызвать только с rvalue, и он будет перемещаться.

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

    При вызове с lvalue это скопирует, чтобы передать аргумент, а затем переход, чтобы заполнить элемент данных. Когда вызывается с rvalue, это переместится для передачи аргумента, а затем переместится для заполнения элемента данных. В случае rvalue перемещение для прохождения аргумента может быть отменено. Таким образом, вызывая это с lvalue, получается одна копия и одно перемещение, и вызов этого с rvalue приводит к одному-двум ходам.

Оптимальным решением является определение как (1), так и (2). Решение (3) может иметь дополнительный ход относительно оптимального. Но писать одну функцию короче и удобнее, чем писать две практически идентичные функции, а движения считаются дешевыми.

При вызове со значением, неявно конвертируемым в строку типа const char*, происходит неявное преобразование, которое включает вычисление длины и копию строковых данных. Затем мы попадаем в случаи rvalue. В этом случае использование string_view предоставляет еще один вариант:

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

    При вызове с строкой lvalue или rvalue это приводит к одной копии. При вызове с const char* выполняется одно вычисление длины и одна копия.

Ответ 3

Короткий ответ сначала: вызов const и всегда будет стоить копию. В зависимости от условий вызов по значению может стоить только один шаг. Но это зависит (пожалуйста, посмотрите примеры кода ниже для сценария, на который ссылается эта таблица):

            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;
};

В случае lvalue, переданного в name_it, вы будете иметь две операции копирования в случае rvalue. Это плохо, потому что rvalue мог меня переместить.

Одним из возможных решений было бы написать перегрузку для rvalues:

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;
};

Это решает проблему, и все в порядке, несмотря на то, что у вас есть две функции кода с точно таким же кодом.

Другим жизнеспособным решением будет использование безупречной пересылки, но это также имеет несколько недостатков (например, совершенные функции пересылки являются довольно жадными и делают бесполезную существующую перегруженную функцию 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. Таким образом, у вас будет одна функция как для rvalue, так и для lvalue. Вы будете перемещать rvalues, но примите дополнительный ход для lvalues, который может быть хорошим , если перемещение дешево, и вы копируете или перемещаете параметр независимо от условий.

Итак, в конце концов, я действительно думаю, что неправильно ошибаться один на один. Это сильно зависит.

#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

Ответ 4

Вне причин производительности, когда экземпляр выдает исключение в конструкторе по значению, он сначала набрасывается на вызывающего, а не внутри самого конструктора. Это упрощает кодирование 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

Ответ 5

Я сделал эксперимент:

#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.