C++ возвращает местонахождение объектов класса значения в памяти после оптимизации
Допустим, есть определенный пользователемclass Foo
. В некоторых сообщениях предполагается, что объект класса C++ «никогда» не размещается в куче, если он не выделен с помощьюnew
. Но! с другой стороны, есть сообщения, которые предполагают, что возврат локально выглядящего объекта класса по значению из функции не обязательно копирует какие-либо данные. Так! где изначально хранились данные такого объекта? Это все еще стек? Был ли он переведен в стек вызывающей функции?
class Foo {
...
}
Foo a(int x) {
Foo result;
doabc(x, result);
return result;
}
Foo b(int x) {
Foo result = a(x);
doxyz(x,result);
return result;
}
int main() {
int x;
cin >> x;
Foo result = b(x);
dosomethingelse(result);
cout << result;
}
Если результат Foo из a не копируется по значению, где он размещается? Куча или стек? Если в куче, компилятор автоматически реорганизует код для вставки удаления? Если в стеке, в каком кадре стека он будет жить? б? Вот сообщение, которое заставляет меня задуматься: /questions/9758028/c11-optimizatsiya-vozvraschaemogo-znacheniya-ili-pereezd/9758054#9758054. Спасибо!
3 ответа
В языке C++ нет кучи или стека. Это свойства реализации, а не языка. Реализация может делать все, что захочет, при условии, что полученная программа делает то, что, согласно стандарту, она должна делать. Это включает в себя выделение памяти для автоматических переменных или временных объектов в куче.
Однако «нормальные» реализации этого не делают. Как автоматические, так и временные переменные размещаются в стеке или в регистрах.
В вашем примере все переменные с именем
Объект может владеть ресурсами, такими как память, которые необходимо выделить в свободном хранилище («куча» в терминах реализаций, в которых есть куча). В этом случае конструктор копирования или оператор присваивания копирования позаботятся о копировании и выделят материал там, где это необходимо. Но речь идет о ресурсах, принадлежащих объекту, а не о самом объекте.
Вопрос, на который вы ссылаетесь, касается предотвращения копирования, в частности, предотвращения копирования принадлежащих ему ресурсов. Это имеет мало общего с чем-либо из вышеперечисленного.
Реализации разрешено, а иногда и требуется, исключать копии. Исключение копирования — один из способов избежать копирования. Вот пример:
#include <iostream>
struct Foo
{
Foo() {}
Foo(const Foo&) { std::cout << "Copied by ctor\n"; };
Foo& operator=(const Foo&) { std::cout << "Copied by assignment\n"; return *this; };
};
Foo func()
{
Foo foo;
return foo;
}
int main()
{
Foo foo (func());
}
Живая демонстрация. В этом примере показано как обязательное, так и необязательное исключение копирования.
В C++14 и ниже есть два случая необязательного исключения копирования. В C++17 и более поздних версиях существует один случай необязательного и один случай обязательного исключения копирования (новая функция в C++17, официально называемая «временной материализацией»). GCC по умолчанию выполняет дополнительное удаление копирования, но его можно отключить с помощью
Другой способ избежать копирования — по возможности использовать перемещение вместо копирования. Семантика перемещения — это отдельная история, выходящая за рамки этого ответа.
На такой вопрос не может быть общего ответа. Если ваш код достаточно прост, хранилище находится не в стеке, а в регистрах.
Если ваша функция возвращает значение, хранилище находится либо в стеке, либо в регистрах — зависит от размера вашего объекта (а также от деталей вашего компилятора).
Возможно, бессмысленно говорить о том, «где находится объект» — компилятор может исключить его из оптимизации. Если вы дизассемблируете скомпилированный код, вы можете обнаружить, где хранятся ваши объекты, но это может потребовать некоторого воображения: например, половина данных объекта может находиться в стеке, а другая половина - в регистрах. Или половина в регистрах, а половина оптимизирована.
Короткий ответ:
Предполагая, что копия не создается (компилятор выполнил NRVO), вызывающая сторона выделит память для объекта в стеке и передаст указатель на него вызываемой стороне, которая вызовет конструктор класса в этом месте.
Это, конечно, не предполагает никаких оптимизаций (за исключением самого NRVO).
Например, в регистре может быть возвращен достаточно маленький и простой (без нетривиальных операций копирования/перемещения?) класс.