Подтвердить что ты не робот

Возвращение больших объектов в функции

Сравните следующие два фрагмента кода, первый с использованием ссылки на большой объект, а второй имеет большой объект в качестве возвращаемого значения. Акцент на "большой объект" относится к тому факту, что повторные копии объекта, без необходимости, являются потерянными циклами.

Использование ссылки на большой объект:

void getObjData( LargeObj& a )
{
  a.reset() ;
  a.fillWithData() ;
}

int main()
{
  LargeObj a ;
  getObjData( a ) ;
}

Использование большого объекта в качестве возвращаемого значения:

LargeObj getObjData()
{
  LargeObj a ;
  a.fillWithData() ;
  return a ;
}

int main()
{
  LargeObj a = getObjData() ;
}

Первый фрагмент кода не требует копирования большого объекта.

Во втором фрагменте объект создается внутри функции, и, как правило, при возврате объекта требуется копия. В этом случае, однако, в main() объект объявляется. Будет ли компилятор сначала создавать объект, построенный по умолчанию, а затем скопировать объект, возвращенный getObjData(), или он будет таким же эффективным, как первый фрагмент?

Я думаю, что второй фрагмент легче читать, но я боюсь, что он менее эффективен.

Изменить: Обычно я рассматриваю случаи LargeObj как универсальные классы контейнеров, которые для аргумента содержат тысячи объектов внутри них. Например,

typedef std::vector<HugeObj> LargeObj ;

поэтому непосредственное изменение/добавление методов к LargeObj не является прямым решением.

4b9b3361

Ответ 1

Второй подход более идиоматичен и выразителен. При чтении кода ясно, что функция не имеет предварительных условий для аргумента (у нее нет аргумента) и что она фактически создаст объект внутри. Первый подход не так понятен для случайного читателя. Вызов подразумевает, что объект будет изменен (передается по ссылке), но это не так ясно, если есть какие-либо предварительные условия для переданного объекта.

О копиях. Код, который вы опубликовали, не использует оператор присваивания, а скорее копирует конструкцию. С++ определяет оптимизацию возвращаемого значения, которая реализована во всех основных компиляторах. Если вы не уверены, что можете выполнить следующий фрагмент в своем компиляторе:

#include <iostream>
class X
{
public:
    X() { std::cout << "X::X()" << std::endl; }
    X( X const & ) { std::cout << "X::X( X const & )" << std::endl; }
    X& operator=( X const & ) { std::cout << "X::operator=(X const &)" << std::endl; }
};
X f() {
    X tmp;
    return tmp;
}
int main() {
    X x = f();
}

С g++ вы получите одну строку X:: X(). Компилятор резервирует пространство в стеке для объекта x, а затем вызывает функцию, которая строит tmp поверх x (фактически tmp - x. Операции внутри f() применяются непосредственно к x, будучи эквивалентно вашему первому фрагменту кода (передать по ссылке).

Если вы не использовали конструктор копирования (если бы вы написали: X x; x = f();), то он создавал бы как x, так и tmp и применял бы оператор присваивания, получая трехстрочный вывод: X:: X()/X:: X()/X:: operator =. Таким образом, это может быть немного менее эффективным в случаях.

Ответ 2

Используйте второй подход. Может показаться, что он менее эффективен, но стандарт С++ позволяет обходить копии. Эта оптимизация называется Именованная оптимизация возвращаемого значения и реализована в большинстве современных компиляторов.

Ответ 3

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

Тем не менее, вы все равно можете использовать второй подход для ясности, даже если данные в объекте большие, без ущерба для производительности при правильном использовании интеллектуальных указателей. Ознакомьтесь с набором классов интеллектуальных указателей в boost. Таким образом, внутренние данные выделяются один раз и никогда не копируются, даже если внешний объект.

Ответ 4

Способ избежать любого копирования - предоставить специальный конструктор. если ты может перезаписать код, чтобы он выглядел так:

LargeObj getObjData()
{
  return LargeObj( fillsomehow() );
}

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

Ответ 5

Несколько идиоматическое решение было бы:

std::auto_ptr<LargeObj> getObjData()
{
  std::auto_ptr<LargeObj> a(new LargeObj);
  a->fillWithData();
  return a;
}

int main()
{
  std::auto_ptr<LargeObj> a(getObjData());
}

Ответ 6

В качестве альтернативы вы можете избежать этой проблемы вместе, разрешив объекту получить свои собственные данные, т.е. е. сделав getObjData() функцией-членом LargeObj. В зависимости от того, что вы на самом деле делаете, это может быть хорошим способом.

Ответ 7

В зависимости от того, насколько велик объект и как часто происходит операция, не становитесь слишком увязшими в эффективности, когда он не будет иметь никакого заметного эффекта в любом случае. Оптимизация за счет чистого, читаемого кода должна происходить только тогда, когда определено, что это необходимо.

Ответ 8

Скорее всего, некоторые циклы будут потрачены впустую, когда вы вернетесь по копии. Стоит ли беспокоиться о том, насколько большой объект на самом деле, и как часто вы вызываете этот код.

Но я хотел бы указать, что если LargeObj - это большой и нетривиальный класс, то в любом случае его пустой конструктор должен инициализировать его в известном состоянии:

LargeObj::LargeObj() :
 m_member1(),
 m_member2(),
 ...
{}

Это тоже тратит несколько циклов. Переписывая код как

LargeObj::LargeObj()
{
  // (The body of fillWithData should ideally be re-written into
  // the initializer list...)
  fillWithData() ;
}

int main()
{
  LargeObj a ;
}

вероятно, будет для вас беспроигрышной: у вас будут экземпляры LargeObj, которые будут инициализированы в известные и полезные состояния, и у вас будет меньше потраченных циклов.

Если вы не всегда хотите использовать fillWithData() в конструкторе, вы можете передать флаг в конструктор в качестве аргумента.

UPDATE (из вашего редактирования и комментария): Семантически, если стоит создать typedef для LargeObj - т.е. дать ему имя, а не ссылаться на него просто как typedef std::vector<HugeObj> - тогда вы уже в пути, чтобы дать ему свою собственную поведенческую семантику. Вы могли бы, например, определить его как

class LargeObj : public std::vector<HugeObj> {
    // constructor that fills the object with data
    LargeObj() ; 
    // ... other standard methods ...
};

Только вы можете определить, подходит ли это для вашего приложения. Я считаю, что даже если LargeObj является "главным образом" контейнером, вы все равно можете дать ему поведение класса, если это работает для вашего приложения.

Ответ 9

Ваш первый фрагмент особенно полезен, когда вы делаете такие вещи, как getObjData(), реализованные в одной DLL, вызывают его из другой DLL, а две библиотеки DLL реализованы на разных языках или разных версиях компилятора для одного и того же языка. Причина в том, что, когда они компилируются в разных компиляторах, они часто используют разные кучи. Вы должны выделять и освобождать память из одной кучи, иначе вы будете повреждать память. </windows>

Но если вы не сделаете что-то подобное, я бы обычно просто возвращал указатель (или интеллектуальный указатель) в память, которую выделяет ваша функция:

LargeObj* getObjData()
{
  LargeObj* ret = new LargeObj;
  ret->fillWithData() ;
  return ret;
}

... если у меня нет конкретной причины не делать этого.