Создание и возвращение большого объекта из функции

Представьте себе такую ​​ситуацию, что у меня есть такая функция:

Object f()
{
    Object obj;
    return obj;
}

Где sizeof(Object) - большое значение.

И затем я вызываю эту функцию:

Object object = f();  

Правильно ли я понимаю, что первый объект будет создан в стеке (в функции), а затем будет скопирован в переменную объекта?

Если это так, разумно ли создать объект в функции в куче и вернуть указатель на него вместо копии?

Но я имею в виду, что объект должен быть создан в функции f() - не передан указателем или ссылкой на эту функцию и не инициализирован.

ИЗМЕНИТЬ

Я не имею в виду, что f - очень простая функция. Он может иметь очень сложную процедуру инициализации объекта в зависимости от некоторого контекста. Будет ли компилятор также оптимизировать его?

Ответ 1

В этом конкретном случае вы можете воспользоваться тем фактом, что компиляторы в настоящее время достаточно умны, чтобы оптимизировать для него. Оптимизация называется с именем оптимизацией возвращаемого значения (NRVO), поэтому вполне нормально возвращать такие "большие" объекты. Компилятор может видеть такие возможности (особенно в том, что так просто, как ваш фрагмент кода), и генерировать двоичный файл, чтобы никакие копии не выполнялись.

Вы также можете вернуть неназванные временные файлы:

Object f()
{
    return Object();
}

Это вызывает (без названия) оптимизацию возвращаемого значения (RVO) практически для всех современных компиляторов С++. На самом деле Visual С++ реализует эту конкретную оптимизацию, даже если все оптимизации отключены.

Эти виды оптимизации специально разрешены стандартом С++:

ISO 14882: 2003 Стандарт С++, §12.8 п. 15: Копирование объектов класса

Когда выполняются определенные критерии, реализации разрешено опустить скопировать построение объекта класса, даже если конструктор копирования и/или деструктор для объекта имеет сторону последствия. В таких случаях реализация рассматривает источник и цель операции с опущенной копией как просто два разных способа ссылаясь на тот же объект, и разрушение этого объекта происходит позже того времени, когда объекты были бы уничтожены без оптимизации. Элисон операций копирования в следующие обстоятельства (которые могут быть в сочетании с копии):

  • в выражении return в функции типа terturn класса, когда выражение является именем энергонезависимый автоматический объект с такой же cv-неквалифицированный тип, как возвращаемый тип функции, копия операция может быть опущена построение автоматического объекта непосредственно в функцию return Значение
  • когда объект временного класса, который не привязан к ссылке будет скопирован в объект класса с тот же самый cv-unqualitied тип, копия операция может быть опущена построение временного объекта прямо в цель пропущенная копия.

Как правило, компилятор всегда будет пытаться реализовать NRVO и/или RVO, хотя в определенных случаях это может не произойти, как несколько путей возврата. Тем не менее, это очень полезная оптимизация, и вы не должны бояться ее использовать.

Если вы сомневаетесь, вы всегда можете проверить свой компилятор, вставив "отладочные заявления" и убедитесь сами:

class Foo
{
public:
    Foo()                      { ::printf("default constructor\n"); }
    // "Rule of 3" for copyable objects
    ~Foo()                     { ::printf("destructor\n");          }
    Foo(const Foo&)            { ::printf("copy constructor\n");    }
    Foo& operator=(const Foo&) { ::printf("copy assignment\n");     } 
};

Foo getFoo()
{
    return Foo();
}

int main()
{
    Foo f = getFoo();
}

Если возвращаемый объект не предназначен для копирования, или (N) RVO не работает (что, вероятно, вряд ли произойдет), вы можете попробовать вернуть прокси-объект:

struct ObjectProxy
{
private:
    ObjectProxy() {}
    friend class Object;    // Allow Object class to grab the resource.
    friend ObjectProxy f(); // Only f() can create instances of this class.
};

class Object
{
public:
    Object() { ::printf("default constructor\n"); }
    ~Object() { ::printf("destructor\n"); }
    // copy functions undefined to prevent copies
    Object(const Object&);
    Object& operator=(const Object&);
    // but we can accept a proxy
    Object(const ObjectProxy&)
    {
        ::printf("proxy constructor\n");
        // Grab resource from the ObjectProxy.
    }
};

ObjectProxy f()
{
    // Acquire large/complex resource like files
    // and store a reference to it in ObjectProxy.
    return ObjectProxy();
}

int main()
{
     Object o = f();
}

Конечно, это не совсем очевидно, поэтому потребуется соответствующая документация (по крайней мере, комментарий об этом).

Вы также можете вернуть какой-либо умный указатель (например, std::auto_ptr или boost::shared_ptr или что-то подобное) объекту, выделенному в свободном хранилище. Это необходимо, если вам нужно вернуть экземпляры производных типов:

class Base {};
class Derived : public Base {};

// or boost::shared_ptr or any other smart pointer
std::auto_ptr<Base> f()
{
    return std::auto_ptr<Base>(new Derived);
}

Ответ 2

В теории вы описываете, что должно произойти. В любом случае компиляторы часто могут оптимизировать его таким образом, что используется вызывающий Object: f будет напрямую писать на объект-вызов и возвращать null.

Это называется Оптимизация возвращаемого значения (или RVO)

Ответ 3

Я правильно понимаю, что сначала Объект будет создан в стеке (в функция), а затем будет скопирована для переменной объекта?

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

Если да, то разумно ли создать объект в функции в куче и для возврата указателя на него вместо копировать?

Да, разумно создать объект в куче и вернуть указатель на него, пока вы четко документируете, что клиент несет ответственность за очистку памяти.

Однако лучше, чем разумно вернуть интеллектуальный указатель, такой как shared_ptr<Object>, который избавляет клиента от необходимости запоминать явное освобождение памяти.

Ответ 4

Компилятор оптимизирует его.

За исключением некоторых ситуаций например:

std::string f(bool cond = false)
{
  std::string first("first");
  std::string second("second");
  // the function may return one of two named objects
  // depending on its argument. RVO might not be applied
  if(cond)
    return first;
  else
    return second;
}

Конечно, могут быть старые компиляторы, которые могут вызвать конструктор копирования. Но вы не должны беспокоиться об этом с современными компиляторами.

Ответ 5

Может ли компилятор применить RVO, зависит от фактического задействованного кода. Общее руководство заключается в том, чтобы создать возвращаемое значение как можно позже. Например:

std::string no_rvo(bool b) {
  std::string t = "true", f = "fals";

  f += t[3];  // Imagine a "sufficiently smart compiler" couldn't delay initialization
  // for some reason, such not noticing only one object is required depending on some
  // condition.

  //return (b ? t : f);  // or more verbosely:
  if (b) {
    return t;
  }
  return f;
}

std::string probably_rvo(bool b) {
  // Delay creation until the last possible moment; RVO still applies even though
  // this is superficially similar to no_rvo.
  if (b) {
    return "true";
  }
  return "false";
}

С С++ 0x компилятор может делать еще больше допущений, главным образом, имея возможность использовать семантику перемещения. Как эти работы являются "черными" червями, но семантика перемещения разрабатывается так, что они могут применяться к точному коду выше. Это очень помогает в случае no_rvo, но в обоих случаях обеспечивает гарантированную семантику, поскольку операция перемещения (если возможно) предпочтительнее операции копирования, тогда как RVO является полностью необязательным и нелегко проверить.

Ответ 6

Если ваша функция f является методом factory, лучше вернуть указатель или инициализированный объект интеллектуального указателя, такой как auto_ptr.

auto_ptr<Object> f()
{
     return auto_ptr<Object>(new Object);
}

Для использования:

{    
    auto_ptr<Object> myObjPtr = f();
    //use myObjPtr . . . 
} // the new Object is deleted when myObjPtr goes out of scope

Ответ 7

Я не знаю, почему никто еще не указал на очевидное решение. Просто передайте выходной объект по ссылке:

void f(Object& result) {
  result.do_something();
  result.fill_with_values(/* */);
};

Таким образом:

  • вы наверняка избегаете копии.

  • вы избегаете использования кучи.

  • вы избегаете оставлять вызывающий код с ответственностью за освобождение динамически выделенного объекта (хотя shared_ptr или unique_ptr тоже это сделают).

Другая альтернатива - сделать функцию членом Object, но это может быть неприемлемо, в зависимости от того, что заключает контракт f().