Как компилятор определяет, когда RVO безопасен?

Как компилятор определяет, когда RVO безопасен? И нет, я имею в виду не rvalue, а lvalue - если я правильно понимаю, RVO работает, "пересылая" целевой адрес методу, поэтому он возвращается на целевой адрес вместо адреса временного, а затем копирует/ назначить цели.

Но как компилятор узнает, что выполнять RVO безопасно? Что, если в lvalue уже есть некоторые данные, включая динамически выделяемый ресурс? RVO в таком случае может привести к утечке ресурса. Может быть, есть какие-то правила, которые определяют, применимо ли выполнение оптимизации или придерживайтесь копирования или присваивания?


person Community    schedule 17.12.2013    source источник
comment
RVO не может привести к утечке ресурса (для вменяемого типа). Объект создается в том же месте, где вызываемый объект ожидает возвращаемое значение. Точные правила могут зависеть от реализации, однако они не предписываются стандартом.   -  person juanchopanza    schedule 17.12.2013
comment
Как это может привести к утечке ресурса? Он ведет себя так, как если бы вы передали переменную, которую вы копируете, в качестве ссылочного параметра   -  person szx    schedule 17.12.2013
comment
Если это функция типа конструктора, которая выделяет динамический ресурс указателю-члену, запуск этой функции дважды по адресу одного и того же объекта будет выделять ресурс дважды, я предполагаю, что функция типа конструктора будет напрямую выделять вместо проверки, если указатель уже распределяет. Что имело бы смысл для функции типа конструктора. Итак, первый ресурс утечет...   -  person    schedule 17.12.2013
comment
Почему конструктор вызывается дважды?   -  person Joseph Mansfield    schedule 17.12.2013
comment
@sftrabbit - ну, я явно вставил его и назвал его функцией типа конструктора, а не конструктором как таковым. Допустим, `T a = foo1(); .... a = foo2()` - для foo1 безопасно применять RVO, но если его применит foo2, произойдет утечка динамических ресурсов.   -  person    schedule 17.12.2013


Ответы (2)


RVO может только инициализировать новый объект, но не переназначать существующий объект. Итак, в этом случае:

Thing thing = make_thing();

адрес thing передается функции, которая инициализирует его на месте.

В этом случае:

thing = make_thing();

РВО (вообще) создает временный, который потом присваивается thing. Не будет никаких утечек или подобных проблем, пока тип правильно назначается. Поскольку это временное rvalue, его можно переместить, что может оказаться более эффективным, чем копирование. Если тип присваивается тривиально, то это присваивание также может быть опущено — компилятор будет знать, так ли это, когда он выбирает, как вызывать функцию.

person Mike Seymour    schedule 17.12.2013
comment
Итак, существует правило rvo, которое можно применять только к новым объектам, но как компилятор определяет, является ли объект новым? Если это только экземпляр и ничего больше? Означает ли это, что даже операции, которые технически не препятствуют rvo, отключат оптимизацию, если они используются с объектом между созданием экземпляра и точкой rvo? - person ; 17.12.2013
comment
@ user2341104: Правила исключения копий и перемещений довольно сложны, но это разумное упрощение. Компилятор может видеть, что объект новый, потому что вызов функции является частью его инициализатора. Если он переназначается (как в моем втором примере), то (в общем) RVO используется для инициализации временного назначения, как я описал. - person Mike Seymour; 17.12.2013
comment
Итак, T t = foo() будет использовать RVO, а T t; t = foo() — нет (между двумя операторами больше ничего не происходит)? Другими словами, нужно ли компилятору, чтобы присваивание было частью создания экземпляра, или, может быть, у него есть другие способы определить, является ли присвоение первым методом, вызываемым после точки создания экземпляра? - person ; 17.12.2013
comment
@ user2341104: Первый случай не присваивание, а инициализация. RVO можно использовать для инициализации, но не (вообще) для присваивания. - person Mike Seymour; 17.12.2013
comment
@user2341104 user2341104 Он (вероятно) будет использовать RVO, но не будет исключать назначение копирования/перемещения из возвращаемого значения в t. Причина, по которой присваивание не исключается, заключается в том, что требуется некоторая работа, чтобы гарантировать, что объект переходит из одного состояния в другое без утечки. Копирование/перемещение можно исключить только во время строительства, потому что еще ничего не выделено. - person Joseph Mansfield; 17.12.2013
comment
Обратите внимание, что случай присваивания по-прежнему выигрывает от конструкторов перемещения, так как безымянный временный объект может быть перемещен. @user2341104: - person MSalters; 17.12.2013
comment
@MikeSeymour - но есть ли такая разница, вы говорите, что это не присваивание, а инициализация, но он все еще использует оператор присваивания, различает ли компилятор два контекста оператора присваивания? Разве присваивание не является инициализацией, если оно используется для неинициализированного объекта, но не для строки его создания? - person ; 17.12.2013
comment
@user2341104 user2341104 Это не оператор присваивания. В объявлении, подобном T t = ...;, = является частью инициализации, а не оператором присваивания. Оператор присваивания может появляться только внутри выражения, а T t = ...; не является выражением. - person Joseph Mansfield; 17.12.2013
comment
@sftrabbit - но он использует оператор присваивания (в случае отсутствия rvo)? Кроме того, мне интересно, как компилятор вообще знает, что делает оператор =, для примитивных типов - конечно, но rvo там действительно не имеет смысла, но для пользовательских типов оператор = может делать все, что пожелает разработчик. - person ; 17.12.2013
comment
@user2341104 user2341104 Он никогда не будет использовать оператор присваивания — он использует конструктор копирования/перемещения. Не позволяйте = обмануть вас. В этом случае T t = foo(); эквивалентно T t(foo());. Вы не можете вызвать оператор присваивания для объекта, который еще не создан. Во-вторых, вы действительно спрашиваете о конструкторах копирования/перемещения, а компилятору все равно, что они делают. Компилятор может игнорировать их в любом случае. Ожидается, что ответственный программист на C++ не будет реализовывать конструкторы копирования/перемещения с побочными эффектами. - person Joseph Mansfield; 17.12.2013
comment
@sftrabbit - позвольте мне убедиться, что я правильно понял. T t1(t2) использует конструктор копирования, как и T t1 = t2, а НЕ оператор присваивания? - person ; 17.12.2013
comment
@ user2341104 Правильно. - person Joseph Mansfield; 17.12.2013
comment
Хм, это кажется нелогичным, я ожидал, что ввод оператора присваивания приведет к присваиванию независимо от контекста. - person ; 17.12.2013
comment
@ user2341104: Действительно, синтаксис C++ часто нелогичен и зависит от контекста в результате эволюции старых языков в течение нескольких десятилетий. = — это всего лишь оператор присваивания в выражении присваивания. Тот же токен также используется в объявлении для обозначения инициализации (и в других контекстах, таких как объявления функций и использование директив с другим значением снова). - person Mike Seymour; 17.12.2013
comment
Кажется, здесь есть некоторая путаница в отношении того, что такое RVO. RVO не зависит от того, что делает вызывающий; это происходит в функции, выполняющей возврат. (Применяются и другие оптимизации, подобные тем, что описаны в разделе об инициализации.) - person James Kanze; 17.12.2013
comment
@user2341104 user2341104 Это не оператор присваивания. В C++ есть несколько символов, которые могут быть как операторами, так и знаками препинания: =, ,, <, >, '(' и как минимум ). В случае объявления = является не оператором, а знаком препинания (кроме случаев типа T t = a = b; , где первый = — знак препинания, а второй — оператор). - person James Kanze; 17.12.2013
comment
@MikeSeymour = означал как присваивание, так и инициализацию с самых первых дней C. Конечно, без определяемых пользователем конструкторов различие на самом деле не очень существенно; компилятор генерирует точно такой же код для int i = 42; и int i; i = 42;, хотя = означает инициализацию в первом случае и присваивание во втором. Но эти два падежа были различны грамматически с самого начала C. - person James Kanze; 17.12.2013
comment
@JamesKanze: Да, я уверен, что этот аспект C всегда был одинаковым, и я совершенно уверен, что не говорил иначе. Я только что сказал, что это (также) имеет место в современном C++ (из-за его эволюции от C), что более актуально для этого вопроса. - person Mike Seymour; 17.12.2013

Оптимизация возвращаемого значения — это частный случай исключения копирования. Это может произойти в следующей ситуации, описанной в стандарте:

в операторе возврата в функции с типом возвращаемого значения класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или предложения catch) с тем же типом cv-unqualified, что и тип возвращаемого значения функции, операцию копирования/перемещения можно опустить, встроив автоматический объект непосредственно в возвращаемое значение функции.

Нет причин, по которым это должно привести к утечке памяти. Если класс выполняет некоторое динамическое выделение в своем конструкторе, это произойдет, когда объект создается непосредственно в возвращаемом функцией значении.


В ответ на ваш комментарий (где foo1 и foo2 создают объекты T и возвращают их):

T a = foo1();
a = foo2();

Здесь мы рассматриваем не только RVO, но и другой вид исключения копии, возникающий при попытке создать объект из временного объекта.

В первой строке можно исключить две копии/хода:

  1. Возврат построенного объекта из foo1
  2. Копирование возвращенного объекта в a

То есть объект, построенный в foo1, может быть создан непосредственно в местоположении a. Если конструктор динамически выделяет некоторый объект, это будет сделано только один раз для объекта a.

Во второй строке можно исключить одиночное копирование/перемещение - только возврат из функции. Таким образом, объект, который создает foo2, будет создан непосредственно в возвращаемом значении функции, а затем будет назначено копирование/перемещение в a. Задания копирования/перемещения не исключаются.

Затем оператор присваивания копирования/перемещения должен гарантировать, что исходный выделенный ресурс будет безопасно удален, а единственным оставшимся ресурсом будет тот, который был создан внутри foo2.

person Joseph Mansfield    schedule 17.12.2013
comment
@user2341104 user2341104 Но он создается только один раз, непосредственно в возвращаемом значении функции. - person Joseph Mansfield; 17.12.2013
comment
посмотри мой комментарий к вопросу - person ; 17.12.2013
comment
@ user2341104 Ответ в моем ответе. - person Joseph Mansfield; 17.12.2013