2016-12-06 11 views
4

Итак, мое понимание семантики перемещения заключается в том, что они позволяют вам переопределять функции для использования с временными значениями (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.

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

+0

Не могли бы вы объяснить, насколько это существенно отличается от моего второго примера выше? –

+2

Представьте, что вы являетесь компилятором, генерирующим код для 'func' ... и могут быть вызовы к нему в других единицах перевода ... вы переходите из параметра или нет? –

ответ

8

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

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

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

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

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

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

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

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

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

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

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

+2

Хорошо, теперь я понимаю. Перемещение семантики в этом случае является «более эффективной копией», которая, тем не менее, имеет место на временно построенном объекте. Можно ли представить примеры ситуаций, когда компилятор не сможет преодолеть такие временные рамки? В моем примере я бы подумал, что это должно быть возможно, но я не знаю, как точно проверить. –

+0

Из этого ответа, что мне больше всего нравится: «Конечно, в идеальном мире оптимизатор всегда удалял бы любую ненужную копию. Иногда, однако, создание временного сложного, включает в себя динамические распределения, а компилятор не видит во всем этом «. Хотя некоторые примеры были бы хорошими, которые показывают, когда для компилятора практически невозможно избежать копий, которые могут привести к перемещению семантики. – jozols

1

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

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

  • После того, как параметр определен как T&& это всем ясно, что это нормально, чтобы украсть его содержимое. Dot. Простой и понятный. В 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) движение и т е р объект будет добавлен для более дешевой цене.Обратите внимание, что объект не умирает в этой строке, и там нет времен.

+0

Приятный пример, который показывает, что вы все равно можете использовать объект после того, как его содержимое было украдено (перемещено). Не уверен, хотя, если я видел это часто в реальных жизненных ситуациях. – jozols

+0

Стандарт явствует, что после перемещения ctor/assign объект должен находиться в правильном состоянии, чтобы его можно было безопасно разрушить. –

3

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

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

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

Это собирается, чтобы получить полную копию-Пропуска здесь. Ничто не копируется.

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

+0

Если класс Bar имеет конструктор, принимающий const std :: vector, то компилятор может, вероятно, генерировать конструктор перемещения, автоматически беря вектор rvalue для этого случая при передаче временного. Это, вероятно, сложно для компилятора, а не в соответствии с текущими стандартами C++, но должно быть возможным. – jozols

2

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

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

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

Этот код создаст временное и передаст ссылку на это временное сообщение на func. Никакое копирование не происходит вообще.

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

Никто. Функция should only take a parameter by rvalue-reference if that function will move from it. Если функция будет наблюдать за значением без его изменения, они берут его на const&, как на C++ 98.

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

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

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

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

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 есть).

+0

О вашем последнем примере: теоретически компилятор мог видеть, что p никогда не используется снова после назначения другому_p, чтобы он мог высвободить копию и просто переместить/переназначить указатели. Я думаю, что это точка зрения автора - он думает, что почти все (если не все) особенности семантики перемещения могут выполняться достаточно умным компилятором, хотя в некоторых случаях это может быть практически невозможно. – jozols