2009-04-15 4 views
35

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

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

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 не является непосредственно доступным решением.

ответ

40

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

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

#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(); 
} 

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

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

+1

Благодарим за отличное объяснение, а также код для «проверки» оптимизации возвращаемого значения. Я могу подтвердить, что компилятор Microsoft Visual Studio 2008 возвращает тот же результат, что и g ++, то есть оптимизированный набор операций. – swongu

+0

Я считаю, что Visual Studio будет включать только RVO в/O2 или выше (например, не в коде отладки, а не только с помощью/O). – Bklyn

+5

Я проверил его с VS2008 и обнаружил, что/Od (отключено) вызывает X :: X() и X :: X (X const &), тогда как/O1/O2/Ox вызывает только X :: X(). Таким образом, при отключенной оптимизации копия выполняется при возврате за пределы f(). – swongu

3

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

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

-1

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

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

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

... если у меня нет особых причин не.

+0

LOL @ в downvote. –

+0

Наверное, нет, по какой-то причине я думал, что OP сделал для нового объекта стека. –

11

Использовать второй подход. Может показаться, что он менее эффективен, но стандарт C++ позволяет обходить копии. Эта оптимизация называется Named Return Value Optimization и реализована в большинстве современных компиляторов.

0

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

+0

Я должен был упомянуть, что в случаях, о которых я обдумываю, LargeObj - это какой-то стандартный контейнерный класс с большим количеством записей (например, std :: vector <>) и не является классом, который я могу изменить. – swongu

+0

В этом случае, я передал бы по ссылке. Мне возвращать экземпляр большого контейнера просто очень плохо. Другим способом было бы иметь класс-оболочку, в который входят контейнер и getObjData(). – Dima

2

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

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

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

+0

Beat мне это :-) –

0

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

0

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

Но я хотел бы отметить, что если 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 (с вашего редактированием & комментария): семантический, если это целесообразно создать ЬурейиЙ для LargeObj - то есть, чтобы дать ему имя, а не ссылаться на него просто как typedef std::vector<HugeObj> - тогда вы уже на пути к предоставлению ей собственной поведенческой семантики. Вы можете, например, определить его как

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

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

+0

Я должен был упомянуть, что в случаях, о которых я обдумываю, LargeObj - это какой-то стандартный контейнерный класс с большим количеством записей (например, std :: vector <>) и не является чем-то что я могу добавить конструктор в. – swongu

+0

Это не исключает добавления конструктора - см. Мое обновление. –

+0

Публично, исходя из контейнеров STL, обычно нахмурились. Отсутствие виртуальных деструкторов является одной из причин, и Google может дать вам больше. – avakar

2

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

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

int main() 
{ 
    std::auto_ptr<LargeObj> a(getObjData()); 
} 
Смежные вопросы