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

Действительно ли std:: move действительно необходим в списке инициализации конструктора для тяжелых членов, переданных по значению?

Недавно я прочитал пример из cppreference.../vector/emplace_back:

struct President
{
    std::string name;
    std::string country;
    int year;

    President(std::string p_name, std::string p_country, int p_year)
        : name(std::move(p_name)), country(std::move(p_country)), year(p_year)
    {
        std::cout << "I am being constructed.\n";
    }

Мой вопрос: действительно ли это std::move? Я хочу сказать, что этот p_name не используется в теле конструктора, поэтому, возможно, есть какое-то правило на языке использовать семантику перемещения по умолчанию?

Было бы очень неприятно добавлять std:: move в список инициализации для каждого тяжелого члена (например, std::string, std::vector). Представьте себе сотни проектов KLOC, написанных на С++ 03, - добавим ли мы везде этот std::move?

Этот вопрос: move-constructor-and-initialization-list отвечает:

Как золотое правило, всякий раз, когда вы принимаете что-то по ссылке rvalue, вы нужно использовать его внутри std:: move, и всякий раз, когда вы принимаете что-либо универсальной ссылки (т.е. выведенного шаблонного типа с & &), вам необходимо используйте его внутри std:: forward

Но я не уверен: передача по значению скорее не универсальная ссылка?

[ОБНОВЛЕНИЕ]

Сделать мой вопрос более ясным. Могут ли аргументы конструктора рассматриваться как XValue - я имею в виду истекающие значения?

В этом примере AFAIK мы не используем std::move:

std::string getName()
{
   std::string local = "Hello SO!";
   return local; // std::move(local) is not needed nor probably correct
}

Итак, нужно ли это здесь:

void President::setDefaultName()
{
   std::string local = "SO";
   name = local; // std::move OR not std::move?
}

Для меня эта локальная переменная - переменная expying - поэтому можно применить семантику переноса... И это похоже на аргументы, переданные значением....

4b9b3361

Ответ 1

Мой вопрос: этот std:: move действительно нужен? Я хочу сказать, что компилятор видит, что это имя p_name не используется в теле конструктора, так, может быть, есть какое-то правило использовать семантику перемещения по умолчанию?

В общем случае, когда вы хотите поменять значение lvalue на rvalue, тогда да, вам нужно std::move(). См. Также Компилируют ли компиляторы С++ 11 локальные переменные в значения r, когда они могут во время оптимизации кода?

void President::setDefaultName()
{
   std::string local = "SO";
   name = local; // std::move OR not std::move?
}

Для меня эта локальная переменная имеет переменную expiring - так что семантика перемещения может применяться... И это похоже на аргументы, переданные по значению....

Здесь я бы хотел, чтобы оптимизатор устранил лишний local ALTOGETHER; к сожалению, на практике это не так. Оптимизация компилятора становится сложной, когда куча памяти входит в игру, см. BoostCon 2013 Keynote: Chandler Carruth: оптимизация появляющихся структур С++, Один из моих выводов из разговоров Чандлера заключается в том, что оптимизаторы просто склонны сдаваться, когда дело доходит до кучи выделенной памяти.

См. приведенный ниже код для неутешительного примера. Я не использую std::string в этом примере, потому что это сильно оптимизированный класс с встроенным ассемблерным кодом, часто приводящий к противоречивому сгенерированному коду. Чтобы добавить травму к оскорблению, std::string, грубо говоря, ссылается на общий подсчитанный указатель в gcc 4.7.2 по крайней мере (оптимизация копирования на запись, теперь запрещенный стандартом 2011 года для std::string). Итак, пример кода без std::string:

#include <algorithm>
#include <cstdio>

int main() {
   char literal[] = { "string literal" };
   int len = sizeof literal;
   char* buffer = new char[len];
   std::copy(literal, literal+len, buffer);
   std::printf("%s\n", buffer);
   delete[] buffer;
}

Очевидно, что согласно правилу "как-если" сгенерированный код может быть оптимизирован для этого:

int main() {
   std::printf("string literal\n");
}

Я пробовал его с GCC 4.9.0 и Clang 3.5 с включенной оптимизацией времени соединения (LTO), и ни один из них не мог оптимизировать код на этот уровень. Я посмотрел на сгенерированный код сборки: оба они выделили память в куче и сделали копию. Ну, да, это разочаровывает.

Распределенная память в стеке отличается:

#include <algorithm>
#include <cstdio>

int main() {
   char literal[] = { "string literal" };
   const int len = sizeof literal;
   char buffer[len];
   std::copy(literal, literal+len, buffer);
   std::printf("%s\n", buffer);
}

Я проверил ассемблерный код: здесь компилятор способен уменьшить код до просто std::printf("string literal\n");.

Таким образом, мои ожидания в том, что избыточный local в вашем примере кода может быть устранен, не полностью не поддерживается: как показывает мой последний пример с выделенным массивом стека, это можно сделать.

Представьте себе сотни проектов KLOC, написанных на С++ 03, - добавим ли мы везде этот std::move?
[...]
Но я не уверен: передача по значению скорее не универсальная ссылка?

"Хотите скорость? Мера" . (Говард Хиннант)

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

std::string getName()
{
   std::string local = "Hello SO!";
   return local; // std::move(local) is not needed nor probably correct
}

Да, но у нас есть правила для этого специального случая: он называется именованной оптимизацией возвращаемого значения (NRVO).

Ответ 2

Текущее правило с поправкой DR1579 заключается в том, что преобразование xvalue происходит, когда локальный или параметр NRVOable или ссылочное выражение к локальной переменной или параметру, является аргументом для оператора return.

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

За исключением того, что не тот случай:

struct S {
    std::string s;
    S(std::string &&s) : s(std::move(s)) { throw std::runtime_error("oops"); }
};

S foo() {
   std::string local = "Hello SO!";
   try {
       return local;
   } catch(std::exception &) {
       assert(local.empty());
       throw;
   }
}

Так что даже для оператора return он фактически не гарантировал, что локальная переменная или параметр, появляющийся в этом выражении, является последним использованием этой переменной.

Не исключено, что стандарт можно изменить, чтобы указать, что "последнее" использование локальной переменной подвержено преобразованию xvalue; проблема заключается в определении того, что такое "последнее" использование. И еще одна проблема заключается в том, что в функции есть нелокальные эффекты; добавление, например, оператор отладки ниже может означать, что преобразование xvalue, на которое вы полагались, больше не происходит. Даже одно единственное правило не будет работать, поскольку один оператор может выполняться несколько раз.

Возможно, вам будет интересно представить предложение для обсуждения в списке рассылки std-предложений?

Ответ 3

Мой вопрос: этот std:: move действительно нужен? Я хочу сказать, что это p_name не используется в теле конструктора, поэтому, может быть, есть какое-то правило на языке использовать семантику перемещения по умолчанию?

Конечно, это нужно. p_name - значение lvalue, поэтому std::move требуется, чтобы превратить его в rvalue и выбрать конструктор перемещения.

Это не только то, что говорит язык - что, если тип такой:

struct Foo {
    Foo() { cout << "ctor"; }
    Foo(const Foo &) { cout << "copy ctor"; }
    Foo(Foo &&) { cout << "move ctor"; }
};

Язык указывает, что copy ctor должен быть напечатан, если вы опустите ход. Здесь нет параметров. Компилятор не может сделать это иначе.

Да, копия elision по-прежнему применяется. Но не в вашем случае (список инициализации), см. Комментарии.


Или возникает вопрос, почему мы используем этот шаблон?

Ответ заключается в том, что он обеспечивает безопасный шаблон, когда мы хотим сохранить копию переданного аргумента, пользуясь ходами и избегая комбинаторного взрыва аргументов.

Рассмотрим этот класс, который содержит две строки (т.е. два "тяжелых" объекта для копирования).

struct Foo {
     Foo(string s1, string s2)
         : m_s1{s1}, m_s2{s2} {}
private:
     string m_s1, m_s2;
};

Итак, посмотрим, что происходит в разных сценариях.

Возьмите 1

string s1, s2; 
Foo f{s1, s2}; // 2 copies for passing by value + 2 copies in the ctor

Арг, это плохо. 4 копии случаются здесь, когда только 2 действительно необходимы. В С++ 03 мы немедленно превратим аргументы Foo() в const-refs.

Возьмите 2

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}

Теперь мы имеем

Foo f{s1, s2}; // 2 copies in the ctor

Это намного лучше!

Но как насчет ходов? Например, из временных:

string function();
Foo f{function(), function()}; // 2 moves + still 2 copies in the ctor

Или, когда явно перемещается lvalues ​​в ctor:

Foo f{std::move(s1), std::move(s2)}; // 2 moves + still 2 copies in the ctor

Это не так хорошо. Мы могли бы использовать string move ctor для инициализации непосредственно членов Foo.

Возьмите 3

Итак, мы могли бы ввести некоторые перегрузки для конструктора Foo:

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(string &&s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(const string &s1, string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(string &&s1, string &&s2) : m_s1{s1}, m_s2{s2} {}

Итак, хорошо, теперь мы имеем

Foo f{function(), function()}; // 2 moves
Foo f2{s1, function()}; // 1 copy + 1 move

Хорошо. Но, черт возьми, мы получаем комбинаторный взрыв: каждый аргумент теперь должен появляться в его вариантах const-ref + rvalue. Что делать, если мы получим 4 строки? Мы собираемся написать 16 ctors?

Возьмите 4 (хороший)

Позвольте вместо этого взглянуть на:

Foo(string s1, string s2) : m_s1{std::move(s1)}, m_s2{std::move(s2)} {}

В этой версии:

Foo foo{s1, s2}; // 2 copies + 2 moves
Foo foo2{function(), function()}; // 2 moves in the arguments + 2 moves in the ctor
Foo foo3{std::move(s1), s2}; // 1 copy, 1 move, 2 moves

Поскольку ходы чрезвычайно дешевы, этот шаблон позволяет им полностью извлечь выгоду и избежать комбинаторного взрыва. Мы действительно можем двигаться полностью.

И я даже не поцарапал поверхность безопасности исключений.


Как часть более общего обсуждения, давайте теперь рассмотрим следующий фрагмент, в котором все задействованные классы делают копию s по значению:

{
// some code ...
std::string s = "123";

AClass obj {s};
OtherClass obj2 {s};
Anotherclass obj3 {s};

// s won't be touched any more from here on
}

Если вы правильно поняли, вам действительно понравится, что компилятор действительно переместил s в свое последнее использование:

{
// some code ...
std::string s = "123";

AClass obj {s};
OtherClass obj2 {s};
Anotherclass obj3 {std::move(s)}; // bye bye s

// s won't be touched any more from here on. 
// hence nobody will notice s is effectively in a "dead" state!
}

Я сказал вам, почему компилятор не может сделать это, но я получаю вашу мысль. Это имело бы смысл с определенной точки зрения - бессмысленно делать s живым дольше, чем его последнее использование. Думаю, пища для размышлений для С++ 2x.

Ответ 4

Я провел еще несколько исследований и опросил другие форумы в сети.

К сожалению, кажется, что этот std::move необходим не только потому, что это говорит стандарт С++, но и в противном случае это было бы опасно:

((кредит для Kalle Olavi Niemitalo из comp.std.С++ - его ответ здесь))

#include <memory>
#include <mutex>
std::mutex m;
int i;
void f1(std::shared_ptr<std::lock_guard<std::mutex> > p);
void f2()
{
    auto p = std::make_shared<std::lock_guard<std::mutex> >(m);
    ++i;
    f1(p);
    ++i;
}

Если f1 (p) автоматически изменено на f1 (std:: move (p)), то мьютекс будет разблокирована уже до второго ++ i; утверждение.

Следующий пример выглядит более реалистичным:

#include <cstdio>
#include <string>
void f1(std::string s) {}
int main()
{
    std::string s("hello");
    const char *p = s.c_str();
    f1(s);
    std::puts(p);
}

Если f1 (s) автоматически изменено на f1 (std:: move (s)), тогда указатель p будет больше недействительным после возвращения f1.