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).

Например, в регистре может быть возвращен достаточно маленький и простой (без нетривиальных операций копирования/перемещения?) класс.

Другие вопросы по тегам