Как C++ ABI справляется с RVO и NRVO?

Меня смущает то, как компилятор и компоновщик справляются с тем фактом, что требования к вызову er функции различаются в зависимости от того, использует ли функция RVO или NRVO.

Это может быть мое недоразумение, но я предполагаю, что вообще без RVO или NRVO

std::string s = get_string();

включает построение перемещения s из результата get_string, если get_string не выполняет N?RVO, но если get_string выполняет N?RVO, вызывающий код ничего не делает, а s создается на месте функцией get_string.

РЕДАКТИРОВАТЬ: вот как я представляю, как работает вызывающая сторона get_string, если нет N?RVO:

  1. вызвать get_string()
  2. Результат get_string теперь находится в стеке, вызывающий использует его для создания s

а теперь с РВО

  1. вызвать get_string()
  2. когда get_string выполняется, в стеке нет результата, get_string создает s, вызывающей стороне не нужно ничего делать для создания s.

person NoSenseEtAl    schedule 23.02.2018    source источник
comment
comment
Не совсем, я знаю, что концептуально делает RVO. Интересно, как это реализовано в ABI, поскольку вызывающий должен знать, как реализован вызываемый (или я ошибаюсь?).   -  person NoSenseEtAl    schedule 23.02.2018
comment
Не знаю, чем он будет сильно отличаться от любого другого звонка. Это стек, поэтому компоновщику все равно. Компилятор просто размещает переменные одну за другой в стеке. Тогда весь доступ к переменной является обратной ссылкой из текущей вершины стека. Переменные, созданные до вызова, будут чуть ниже.   -  person Galik    schedule 23.02.2018
comment
@Galik - отредактированный вопрос   -  person NoSenseEtAl    schedule 24.02.2018


Ответы (1)


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

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

По сути, без каких-либо исключений вы можете думать о вызове из OP примерно так (игнорируйте любые проблемы с псевдонимами, все это фактически будет реализовано непосредственно в сборке):

void get_string(void* retval)
{
    std::string ret;
    // do stuff to ret
    new(retval) std::string(std::move(ret));
}

char retval[sizeof(std::string)];
get_string(retval);
std::string s(std::move(*(string*)retval));

Строка ret копируется (или в данном случае перемещается) дважды: один раз из ret в буфер retval и один раз из retval в s.

Теперь с применением NRVO изменится только определение get_string:

void get_string(void* retval)
{
    std::string& ret = *new(retval) std::string;
    // do stuff to ret
}

С точки зрения звонившего ничего не изменилось. Функция просто напрямую инициализирует объект, который она собирается вернуть, в пространство, выделенное вызывающей стороной для возвращаемого значения. Теперь строка перемещается только один раз: с retval на s.

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

char retval[sizeof(std::string)];
get_string(retval);
std::string& s(*(string*)retval);

Таким образом, s инициализируется непосредственно get_string, и никакие копии или перемещения не выполняются.

person Miles Budnek    schedule 23.02.2018