Как вызывающая функция знает, использовалась ли Оптимизация возвращаемого значения?

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

Например, код

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++ делает эти оптимизации необязательными.