Почему необходима семантика перемещения для исключения временных копий?

Итак, мое понимание семантики перемещения состоит в том, что они позволяют вам переопределять функции для использования с временными значениями (rvalues) и избегать потенциально дорогостоящих копий (путем перемещения состояния из безымянного временного значения в ваше именованное lvalue).

У меня вопрос: зачем для этого нужна особая семантика? Почему компилятор C ++ 98 не может исключить эти копии, поскольку именно компилятор определяет, является ли данное выражение lvalue или rvalue? В качестве примера:

void func(const std::string& s) {
    // Do something with s
}

int main() {
    func(std::string("abc") + std::string("def"));
}

Даже без семантики перемещения C ++ 11 компилятор все равно должен иметь возможность определить, что выражение, переданное в func(), является rvalue, и, следовательно, копия из временного объекта не нужна. Так зачем вообще есть различие? Похоже, что это применение семантики перемещения по сути является вариантом исключения копирования или других подобных оптимизаций компилятора.

В качестве другого примера, зачем вообще нужен такой код?

void func(const std::string& s) {
    // Do something with lvalue string
}

void func(std::string&& s) {
    // Do something with rvalue string
}

int main() {
    std::string s("abc");

    // Presumably calls func(const std::string&) overload
    func(s);

    // Presumably calls func(std::string&&) overload
    func(std::string("abc") + std::string("def"));
}

Кажется, что перегрузка const std::string& может обрабатывать оба случая: lvalues ​​как обычно и rvalues ​​как константную ссылку (поскольку временные выражения по определению являются своего рода константными). Поскольку компилятор знает, когда выражение является lvalue или rvalue, он может решить, опускать ли копию в случае rvalue.

В принципе, почему семантика перемещения считается особой, а не просто оптимизацией компилятора, которую могли бы выполнить компиляторы до C ++ 11?


person AUD_FOR_IUV    schedule 06.12.2016    source источник
comment
Не могли бы вы объяснить, чем это существенно отличается от моего второго примера выше?   -  person AUD_FOR_IUV    schedule 06.12.2016
comment
Представьте, что вы компилятор, генерирующий код для func ... и могут быть обращения к нему в других единицах перевода ... вы выходите из параметра или нет?   -  person M.M    schedule 06.12.2016


Ответы (4)


Функции перемещения точно не исключают временные копии.

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

Формальная объектная модель C ++ никак не изменяется семантикой перемещения. У объектов по-прежнему есть четко определенное время жизни, начиная с определенного адреса и заканчивая тем, что они там уничтожаются. Они никогда не «двигаются» в течение своей жизни. Когда они «перемещаются от», на самом деле происходит то, что кишки извлекаются из объекта, который должен скоро умереть, и эффективно помещаются в новый объект. Может показаться, что они переместились, но формально на самом деле это не так, поскольку это полностью сломало бы C ++.

Перемещение - не смерть. Перемещение требуется, чтобы оставить объекты в «допустимом состоянии», в котором они все еще живы, а деструктор всегда будет вызываться позже.

Удаление копий - это совсем другое дело, когда в некоторой цепочке временных объектов некоторые промежуточные объекты пропускаются. Компиляторы не обязательны для исключения копий в C ++ 11 и C ++ 14, им разрешено делать это, даже если это может нарушить правило «как если бы» это обычно направляет оптимизацию. То есть, даже если ctor копирования может иметь побочные эффекты, компилятор при высоких настройках оптимизации все равно может пропускать некоторые временные файлы.

Напротив, "гарантированное копирование эллисии" - это новая функция C ++ 17, что означает, что стандарт требует, чтобы в определенных случаях выполнялось копирование эллипса.

Семантика перемещения и копирование дают два разных подхода к обеспечению большей эффективности в этих сценариях «цепочки временных файлов». В семантике перемещения все временные библиотеки все еще существуют, но вместо вызова конструктора копирования мы можем вызвать (надеюсь) менее затратный конструктор, конструктор перемещения. В копировальной эллисии мы можем пропустить некоторые объекты вместе.

В принципе, почему семантика перемещения считается особой, а не просто оптимизацией компилятора, которую могли бы выполнить компиляторы до C ++ 11?

Семантика перемещения не является «оптимизацией компилятора». Это новая часть системы типов. Семантика перемещения происходит даже тогда, когда вы компилируете с -O0 на gcc и clang - это вызывает вызов разных функций, потому что тот факт, что объект вот-вот умрет, теперь "аннотируется" в типе ссылка. Он позволяет «оптимизацию на уровне приложения», но это отличается от того, что делает оптимизатор.

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

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

person Chris Beck    schedule 06.12.2016
comment
Хорошо, теперь я понимаю. Семантика перемещения в этом случае является более эффективной копией, которая, тем не менее, имеет место на временно созданном объекте. Не могли бы вы привести примеры ситуаций, когда компилятор не сможет исключить такие временные файлы? Я бы подумал в своем примере, что это должно быть возможно, но я не знаю, как точно проверить. - person AUD_FOR_IUV; 06.12.2016
comment
Из этого ответа мне больше всего нравится: конечно, в идеальном мире оптимизатор всегда удалял бы все ненужные копии. Однако иногда создание временного объекта является сложным, включает в себя динамическое выделение памяти, и компилятор не видит всего этого. Хотя было бы неплохо привести несколько примеров, которые показывают, когда компилятору практически невозможно избежать копий, которых может избежать семантика перемещения. - person jozols; 28.02.2018

Семантика копирования и перемещения не совсем то же самое. При копировании не копируется весь объект, он остается на месте. При движении «что-то» все равно копируется. Копия на самом деле не устранена. Но это «что-то» - бледная тень того, что должна утащить полноценная копия.

Простой пример:

class Bar {

    std::vector<int> foo;

public:

    Bar(const std::vector<int> &bar) : foo(bar)
    {
    }
};

std::vector<int> foo();

int main()
{
     Bar bar=foo();
}

Удачи в попытках заставить ваш компилятор удалить копию здесь.

Теперь добавьте этот конструктор:

    Bar(std::vector<int> &&bar) : foo(std::move(bar))
    {
    }

И теперь объект в main() создается с помощью операции перемещения. Полная копия на самом деле не была удалена, но операция перемещения - это просто некоторый линейный шум.

С другой стороны:

Bar foo();

int main()
{
     Bar bar=foo();
}

Здесь будет полная копия. Ничего не копируется.

В заключение: семантика перемещения на самом деле не исключает и не удаляет копию. Это просто делает получившуюся копию «меньше».

person Sam Varshavchik    schedule 06.12.2016
comment
Если в классе Bar есть конструктор, принимающий const std :: vector &, тогда компилятор, вероятно, мог бы сгенерировать конструктор перемещения, автоматически принимая вектор rvalue для этого случая при передаче временного значения. Это, вероятно, сложно сделать компилятору и не в соответствии с текущими стандартами C ++, но должно быть возможным. - person jozols; 28.02.2018

У вас есть фундаментальное непонимание того, как работают определенные вещи в C ++:

Даже без семантики перемещения C ++ 11 компилятор все равно должен уметь определять, что выражение, переданное в func (), является rvalue, и, следовательно, копия из временного объекта не нужна.

Этот код не вызывает никакого копирования, даже в C ++ 98. const& - это ссылка, а не значение. И поскольку это const, он вполне может ссылаться на временное. Таким образом, функция, принимающая const string& никогда, получает копию параметра.

Этот код создаст временный объект и передаст ссылку на него в func. Копирование вообще не происходит.

В качестве другого примера, зачем использовать такой код?

Никто не делает. Функция должен принимать параметр по ссылке rvalue только в том случае, если эта функция будет перемещаться от него. Если функция собирается только наблюдать значение, не изменяя его, они принимают его на const&, как в C ++ 98.

Самое главное:

Итак, мое понимание семантики перемещения состоит в том, что они позволяют вам переопределять функции для использования с временными значениями (rvalues) и избегать потенциально дорогостоящих копий (путем перемещения состояния из безымянного временного значения в ваше именованное lvalue).

Ваше понимание неверно.

Перемещение касается не исключительно временных значений; если бы это было так, у нас не было бы std::move, позволяющего перейти от lvalues. Перемещение - это передача владения данными от одного объекта к другому. Хотя это часто происходит с временными библиотеками, это также может происходить с lvalue:

std::unique_ptr<T> p = ...
std::unique_ptr<T> other_p = std::move(p);
assert(p == nullptr); //Will always be true.

Этот код создает unique_ptr, а затем перемещает содержимое этого указателя в другой объект unique_ptr. Это не касается временных конструкций; он передает владение внутренним указателем другому объекту.

Это не то, что компилятор мог сделать выводом о том, что вы хотели сделать. Вы должны явно заявить, что хотите выполнить такой переход с lvalue (вот почему там std::move).

person Nicol Bolas    schedule 06.12.2016
comment
О вашем последнем примере: теоретически компилятор мог бы увидеть, что p больше не используется после назначения other_p, поэтому он может исключить копию и просто переместить / переназначить указатели. Я думаю, что в этом суть автора - он думает, что почти все (если не все) функции семантики перемещения могут быть выполнены достаточно умным компилятором, хотя в некоторых случаях это может быть практически невозможно. - person jozols; 28.02.2018

Ответ заключается в том, что семантика перемещения была введена не для удаления копий. Он был введен, чтобы позволить / продвигать более дешевое копирование. Например, если все члены данных класса являются простыми целыми числами, семантика копирования будет такой же, как и семантика перемещения. В этом случае нет смысла определять оператор перемещения и оператор присваивания перемещения для этого класса. Перемещение ctor и назначение перемещения имеют смысл, когда в классе есть что-то, что можно переместить.

На эту тему написано множество статей. Тем не менее некоторые примечания:

  • Когда параметр указан как T&&, всем становится ясно, что украсть его содержимое - это нормально. Точка. Просто и понятно. В C ++ 03 не было четкого синтаксиса или какого-либо другого установленного соглашения, чтобы передать эту идею. На самом деле, есть масса других способов выразить то же самое. Но комитет пошел по этому пути.
  • Семантика перемещения полезна не только для rvalue ссылок. Его можно использовать везде, где вы хотите указать, что хотите передать свой объект функции, и эта функция может принимать его содержимое.

У вас может быть такой код:

void Func(std::vector<MyComplexType> &v)
{
    MyComplexType x;
    x.Set1();          // Expensive function that allocates storage
                       // and computes something.
    .........          // Ton of other code with if statements and loops
                       // that builds the object x.

    v.push_back(std::move(x));  // (1)

    x.Set2();         // Code continues to use x. This is ok.        
}

Обратите внимание, что в строке (1) будет использоваться ctor перемещения, и объект будет добавлен по более низкой цене. Обратите внимание, что объект не умирает на этой линии и там нет временных объектов.

person Kirill Kobelev    schedule 06.12.2016
comment
Хороший пример, который показывает, что вы все еще можете использовать объект после того, как его содержимое было украдено (перемещено). Не уверен, часто ли я видел это в реальных жизненных ситуациях. - person jozols; 28.02.2018
comment
Стандарт явно указывает, что после перемещения ctor / присвоения объект должен быть в правильном состоянии, чтобы его можно было безопасно уничтожить. - person Kirill Kobelev; 28.02.2018