Как вызывающая функция знает, использовалась ли Оптимизация возвращаемого значения?
Мое понимание оптимизации возвращаемого значения заключается в том, что компилятор тайно передает адрес объекта, в котором будет храниться возвращаемое значение, и вносит изменения в этот объект вместо локальной переменной.
Например, код
std::string s = f();
std::string f()
{
std::string x = "hi";
return x;
}
СТАВИТСЯ на
std::string s;
f(s);
void f(std::string& x)
{
x = "hi";
}
При использовании RVO. Это означает, что интерфейс функции изменился, так как есть дополнительный скрытый параметр.
Теперь рассмотрим следующий случай, который я украл из Википедии
std::string f(bool cond)
{
std::string first("first");
std::string second("second");
// the function may return one of two named objects
// depending on its argument. RVO might not be applied
return cond ? first : second;
}
Предположим, что компилятор применит RVO к первому случаю, но не к этому второму случаю. Но не меняется ли интерфейс функции в зависимости от того, применяется ли RVO? Если тело функции f
не видно компилятору, как компилятор знает, было ли применено RVO и должен ли вызывающий объект передать параметр скрытого адреса?
Ответы
Ответ 1
В интерфейсе нет изменений. Во всех случаях результаты
функции должна появиться в области вызывающего абонента;
Обычно компилятор использует скрытый указатель. Единственный
разница заключается в том, что когда используется RVO, как и в вашем первом случае,
компилятор будет "слить" x
и это возвращаемое значение, построив
x
по адресу, указанному указателем; когда он не используется,
компилятор сгенерирует вызов конструктору копирования в
return, чтобы скопировать что-либо в это возвращаемое значение.
Могу добавить, что ваш второй пример не очень близок к тому, что
случается. На сайте вызова вы почти всегда получаете что-то
как:
<raw memory for string> s;
f( &s );
И вызываемая функция либо построит локальную переменную
или временно, непосредственно по адресу, который он передал, или скопировать
постройте некоторое значение по этому адресу. Так что в последний раз
Например, оператор возврата будет более или менее
эквивалент:
if ( cont ) {
std::string::string( s, first );
} else {
std::string::string( s, second );
}
(Показывает неявный указатель this
, переданный в копию
конструктор.) В первом случае, если применяется RVO, специальный
код будет в конструкторе x
:
std::string::string( s, "hi" );
а затем заменяя x
на *s
всюду в функции
(и ничего не делая при возврате).
Ответ 2
Давайте играть с NRVO, RVO и скопировать elision!
Вот тип:
#include <iostream>
struct Verbose {
Verbose( Verbose const& ){ std::cout << "copy ctor\n"; }
Verbose( Verbose && ){ std::cout << "move ctor\n"; }
Verbose& operator=( Verbose const& ){ std::cout << "copy asgn\n"; }
Verbose& operator=( Verbose && ){ std::cout << "move asgn\n"; }
};
это довольно многословно.
Вот функция:
Verbose simple() { return {}; }
это довольно просто и использует прямое построение возвращаемого значения. Если бы в Verbose
отсутствовал конструктор копирования или перемещения, вышеприведенная функция сработала бы!
Вот функция, которая использует RVO:
Verbose simple_RVO() { return Verbose(); }
здесь безымянному временному объекту Verbose()
говорят скопировать себя в возвращаемое значение. RVO означает, что компилятор может пропустить эту копию и напрямую Verbose()
в возвращаемое значение, если и только если есть конструктор копирования или перемещения. Конструктор копирования или перемещения не вызывается, а удаляется.
Вот функция, которая использует NRVO:
Verbose simple_NRVO() {
Verbose retval;
return retval;
}
Чтобы произошла NRVO, каждый путь должен возвращать один и тот же объект, и вы не можете быть хитрым по этому поводу (если вы приведете возвращаемое значение к ссылке, то вернете эту ссылку, которая заблокирует NRVO). В этом случае, что компилятор делает построить именованный объект retval
непосредственно в месте возвращаемого значения. Подобно RVO, конструктор копирования или перемещения должен существовать, но не вызываться.
Вот функция, которая не может использовать NRVO:
Verbose simple_no_NRVO(bool b) {
Verbose retval1;
Verbose retval2;
if (b)
return retval1;
else
return retval2;
}
поскольку есть два возможных именованных объекта, которые он мог бы вернуть, он не может создать их оба в расположении возвращаемого значения, поэтому он должен сделать фактическую копию. В C++ 11 возвращаемый объект будет неявно move
вместо копирования, поскольку это локальная переменная, возвращаемая из функции в простом операторе возврата. Так что есть хотя бы это.
Наконец, на другом конце есть копия elision:
Verbose v = simple(); // or simple_RVO, or simple_NRVO, or...
Когда вы вызываете функцию, вы предоставляете ей ее аргументы и сообщаете ей, куда ей следует поместить возвращаемое значение. Вызывающая сторона отвечает за очистку возвращаемого значения и выделение памяти (в стеке) для него.
Это взаимодействие каким-то образом осуществляется через соглашение о вызовах, часто неявно (т.е. Через указатель стека).
При многих соглашениях о вызовах место, где может храниться возвращаемое значение, может в конечном итоге использоваться как локальная переменная.
В общем, если у вас есть переменная вида:
Verbose v = Verbose();
подразумеваемая копия может быть исключена - Verbose()
создается непосредственно в v
, а не создается временно, а затем копируется в v
. Таким же образом, возвращаемое значение simple
(или simple_NRVO
, или что-то еще) может быть исключено, если модель времени выполнения компилятора поддерживает его (и обычно это делает).
По сути, вызывающий сайт может сказать simple_*
поместить возвращаемое значение в определенную точку и просто обработать эту точку как локальную переменную v
.
Обратите внимание, что NRVO и RVO, а также неявное перемещение выполняются внутри функции, и вызывающей стороне ничего не нужно знать об этом.
Точно так же, исключение на вызывающем сайте выполняется за пределами функции, и если соглашение о вызовах поддерживает его, вам не требуется никакой поддержки со стороны тела функции.
Это не должно быть правдой в каждом соглашении о вызовах и модели времени выполнения, поэтому стандарт C++ делает эти оптимизации необязательными.