Где хранится объект возврата?

Я обычно понимаю, как функция возвращает объект по значению. Но я хотел понять это на более низком уровне. Уровень сборки, если это разумно.

Я понимаю этот код

ClassA fun(){
    ClassA a;
    a.set(...);
    return a;
}

внутренне преобразуется в

void fun(Class& ret){
    ClassA a;
    a.set(...);
    ret.ClassA::ClassA(a);
}

Который эффективно вызывает конструктор копирования по возвращаемому значению.

Я также понимаю, что есть некоторые оптимизации (например, NRVO), которые могут генерировать следующий код, избегая конструктора копирования.

void fun(Class& ret){
    ret.set(...);
}

Однако мой вопрос немного более общий. Это не связано с конкретными объектами. Это могут быть даже примитивные типы.

Предположим, что у нас есть этот код:

int fun(){
   return 0;
}
int main(){
    fun();
}

Мой вопрос в том, где находится возвращаемый объект, хранящийся в памяти.

Если мы посмотрим на стек... Существует стек стека main, а затем фрейм стека fun. Является ли возвращаемый объект сохраненным в некотором адресе, например, между двумя фреймами стека? Или, возможно, он хранится где-то в кадре стека main (и, возможно, тот адрес, который передается по ссылке в сгенерированном коде).

Я подумал об этом, и второй кажется более практичным, но я не понимаю, как компилятор знает, сколько памяти нужно вставить в стек кадров main? Рассчитывает ли он самый большой тип возврата и толкает его, хотя может быть какая-то потерянная память? Или это выполняется динамически, оно выделяет это пространство только до того, как вызывается функция?

Ответ 1

Спецификация языка С++ не определяет эти данные низкого уровня. Они определяются каждой реализацией С++, а фактическая информация о реализации варьируется от платформы к платформе.

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

Также возможно, что данная реализация С++ по-прежнему будет использовать регистр CPU для возврата классов, которые достаточно малы, чтобы вписаться в один регистр CPU. Или, может быть, несколько регистров процессора зарезервированы для возврата немного больших классов.

Детали различаются, и вам нужно будет ознакомиться с документацией для вашего компилятора С++ или вашей операционной системы, чтобы определить конкретные данные, которые относятся к вам.

Ответ 2

Ответ ABI специфичен, но, как правило, вызовы компилируются со скрытым параметром, который является указателем на память, которую должна использовать функция, например, вы сказали, что функция скомпилирована как

void fun(Class& ret){
    ClassA a;
    a.set(...);
    ret.ClassA::ClassA(a);
}

Затем на сайте вызова у вас будет что-то вроде

Class instance = fun();
fun(instance);

Теперь это заставляет зарезервировать sizeof(Class) байт в стеке и передать этот адрес функции, чтобы fun мог "заполнить" это пространство.

Это ничем не отличается от того, как кадр стека вызывающего абонента зарезервировал место для своих собственных локалей, единственное отличие состоит в том, что адрес одного из его локалей передается на fun.

Помните, что если sizeof(Class) меньше размера регистра (или нескольких регистров), вполне возможно, что значение возвращается непосредственно внутри них.

Ответ 3

В следующем коде:

int fun()
{
   return 0;
}

Возвращаемое значение сохраняется в регистре. В архитектуре Intel это обычно будет ax (16-разрядный), или eax (32-разрядный), или rax (64-разрядный). (Исторически известный как аккумулятор.)

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

Если возвращаемое значение больше машинного слова, тогда ABI (Application Binary Interface) может потребовать использования другого регистра для хранения слова высокого порядка. Итак, если вы возвращаете 32-битное количество в 16-битной архитектуре, будет использоваться dx:ax. (И так далее для больших объемов в больших архитектурах.)

Большие возвращаемые значения передаются другими способами, такими как механизм void fun(Class& ret), о котором вы уже знаете.

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