Представьте себе такую ситуацию, что у меня есть такая функция:
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()
.