#c #c 11 #copy-elision
#c #c 11 #исключение копирования
Вопрос:
#include <iostream>
using namespace std;
class A
{
public :
A()
{
cout<<"constructor is called"<<endl;
}
~A()
{
cout<<"destructor is called"<<endl;
}
A(const A amp;s)
{
cout<<"copy constructor is called"<<endl;
}
};
A beta()
{
A a;
cout<<"mem location a : "<<amp;a<<endl;
return a;
}
int main(int argc, char** argv) {
A b = beta();
cout<<"mem location b : "<<amp;b<<endl;
return 0;
}
Вышеупомянутая программа генерирует следующий вывод :
constructor is called
mem location a : 0x7ffc12bdaf77
mem location b : 0x7ffc12bdaf77
destructor is called
Насколько я понимаю, был создан только один экземпляр A, а не 2 экземпляра для A a и A b из-за исключения копирования или оптимизации возвращаемого значения.
Но, рассматривая приведенную выше программу с точки зрения памяти, объект a находится внутри записи активации стека или стекового пространства функции beta и имеет ячейку памяти 0x7ffc12bdaf77 . Когда он возвращается из бета-версии, исключение копирования делает объект b таким же, как объект a, а не копирует его. Итак, теперь b также имеет адрес 0x7ffc12bdaf77 . Я не могу понять, если b по-прежнему является локальной функцией main и присутствует внутри ее стекового пространства, как она может занимать память за пределами стекового пространства main?
Ответ №1:
Рассмотрим эту строку: A b = beta();
. Как компилятор реализует это?
Ну, beta
есть какая-то другая функция. И beta
имеет возвращаемое значение, которое является A
. Итак, есть два возможных способа реализовать это. Компилятор мог бы заставить beta
выделить пространство стека для своего A
возвращаемого значения, но это может быть проблематично, поскольку вызывающему объекту необходимо использовать это возвращаемое значение. Таким образом, вместо этого компилятор заставляет вызывающую среду выделить пространство стека для возвращаемого значения. В конце концов, вызывающий объект знает размер / выравнивание возвращаемого значения, поэтому у него есть все, что ему нужно знать, чтобы выделить это пространство.
Итак, давайте остановимся на последнем. Это означает, что при вызове компилятора beta
он передает адрес, по которому beta
будет возвращаемое значение. Но это также означает, что компилятор для этого конкретного вызова beta
мог бы просто дать beta
возвращаемому значению тот же адрес, который он даст b
.
Итак, прямо здесь мы исключили копию из возвращаемого значения функции в b
.
Итак, когда компилятор переходит к компиляции beta
, он знает, что вызывающий объект собирается указать ему, куда должно идти возвращаемое значение. Таким образом, return a;
оператор семантически копирует из a
переменной в этот объект возвращаемого значения.
Но компилятор может видеть всю beta
. И он может видеть, что a
переменная является локальной переменной, и она возвращается по всем путям управления. Таким образом, вместо того, чтобы указывать a
отдельный адрес стека, компилятор мог бы просто поместить a
возвращаемое значение в память, предоставленную вызывающим объектом.
Итак, снова мы исключили копию из a
в возвращаемое значение.
Ответ №2:
В соглашении о вызовах Microsoft x64 скрытый указатель будет добавлен в качестве первого аргумента к beta
. Этот указатель содержит адрес b
, который будет находиться во фрейме стека main
. Этот указатель будет использоваться для немедленного создания обоих a
и b
одновременно во фрейме стека main
. Другими словами, a
не существует нигде во фрейме стека beta
; с точки зрения памяти a
и b
будут эквивалентны.
Комментарии:
1. Это меня удивляет. Как компиляторы, которые могут видеть только объявление функции, узнают об этом скрытом указателе? Или всегда передается указатель для необязательного заполнения возвращаемого значения, исключенного при копировании?
2. @AsteroidsWithWings: Потому что именно так компилятор компилирует функции, которые возвращают значение (или, скорее, значение, которое либо слишком большое, либо слишком сложное, чтобы поместиться в регистр). Это часть соглашения о вызовах, и это полностью определяется объявлением функции.
3. @NicolBolas Хорошо, значит, это встроенный механизм реализации возвращаемых значений этим ABI во всех случаях? Имеет смысл. Я был сбит с толку, потому что до C 17 NRVO был «оптимизацией», и поэтому вам обычно требовалось определение функции, чтобы знать, можно ли это выполнить (и даже тогда это «могло» не быть выполнено). Но, по дальнейшему размышлению, я полагаю, что никогда не было никакого другого способа реализовать это в целом? Именно по этой причине. 😀
4. Тогда мне приходит в голову, что правила «гарантированного исключения» C 17 требуют, чтобы это было так, и что, следовательно, каждый C ABI должен работать таким образом, а не только соглашение о вызовах MS x64? (поскольку я не знаю ни о какой поломке ABI для C 17 в какой-либо основной игровой форме)
5. @AsteroidsWithWings: Помните, что, согласно C , существует две копии: копия из возврата в объект возвращаемого значения и копия из объекта возвращаемого значения в переменную стека на принимающей стороне. Реализация функции может исключить одну, а получатель может исключить другую. Если оба исключены, то происходит полное исключение.
Ответ №3:
Насколько я понимаю, был создан только один экземпляр A, а не 2
Правильно.
как это может занимать память за пределами стекового пространства main?
Это не так.
Возвращаемое значение просто создается во фрейме стека main
вызова.