C++: как повысить производительность пользовательского класса, который будет часто копироваться?
Я перехожу на C++ из Java, и у меня есть много проблем с пониманием основ работы классов C++ и лучших практик для их проектирования. В частности, мне интересно, должен ли я использовать указатель на моего члена класса в следующем случае.
У меня есть пользовательский класс Foo, который представляет состояние игры на определенном ходу, и Foo имеет переменную-член пользовательского класса Bar, которая представляет логическое подмножество этого состояния игры. Например Foo представляет собой шахматы доска и БАР представляют фигуры, подвергающиеся атаке, и их бегство (не мой конкретный случай, но более универсальная аналогия, я думаю).
Я хотел бы искать последовательность ходов, копируя Foo и обновляя состояние копии соответственно. Когда я закончу поиск этой последовательности ходов, я отброшу эту копию и все еще буду иметь свой оригинальный Foo, представляющий текущее состояние игры.
In Foo.h я объявляю свой класс Foo и объявляю переменную-член для него типа Бар:
class Foo {
Bar b;
public:
Foo();
Foo(const Foo& f);
}
Но в реализации моих конструкторов Foo я вызываю конструктор Bar с некоторыми аргументами, специфичными для текущего состояния, которое я буду знать во время выполнения. Насколько я понимаю, это означает, что конструктор Bar вызывается дважды - один раз, потому что я написал "Bar b;" выше (который вызывает конструктор по умолчанию, если я правильно понимаю), и один раз, потому что я пишу что-то вроде "b = Bar(arg1, arg2,...) "в Foo:: Foo() и Foo:: Foo (const Foo& f).
Если я попытка сделать как можно больше копий Foo в секунду, это проблема, не так ли?
Я думаю, что простое решение состоит в том, чтобы объявить указатель на бар вместо: "Bar *b", который должен избегать создания экземпляра b дважды. Это хорошая практика? Есть ли в этом какие-то подводные камни, о которых я должен знать? Есть ли лучшее решение? Я не могу найти конкретный пример, чтобы помочь мне (кроме множества предупреждений против использования указателей), и вся информация о проектировании классов действительно подавляющая, так что любое руководство будет очень ценно!
EDIT: Да, у меня будет вся информация, необходимая для создания Bar, когда я создам свой Foo. Я думаю, что все это предположили, но чтобы было понятно, у меня уже есть что-то вроде этого для моего конструктора по умолчанию:
Foo(int k=5);
И в Фу.cpp:
Foo::Foo(int k) {
b = Bar(k);
...
}
А затем my Foo и его член бара обновляются постепенно по мере изменения состояния игры.
Поэтому вызов моего пользовательского конструктора панели в моем конструкторе Foo декларация список инициализации выглядит как лучший способ сделать это. Спасибо за ответы!
5 ответов:
В идеале у вас будет вся информация, необходимая для настройки
Bar
в момент построенияFoo
. Лучшим решением было бы что-то вроде:class Foo { Bar b; public: Foo() : b() { ... }; Foo(const Foo& f) : b(f.a, f.b) { ... }; }
Подробнее осписках инициализации конструктора (который не имеет прямого эквивалента в Java.)
Использование указателя
pb = new Bar(arg1, arg2)
На самом деле, скорее всего, ухудшит вашу производительность, так как выделение кучи (которое может включать, среди прочего, блокировку) может легко стать более дорогостоящим, чем назначение построенное не по умолчаниюBar
к построенному по умолчаниюBar
(в зависимости, конечно, от того, насколько сложен вашBar::operator=
.)
Компиляторы очень хорошо оптимизируют ненужное копирование (стандарт позволяет избежать вызовов конструктора копирования, даже если определен пользовательский copycon). Реализация коротких функций в заголовочных файлах также позволяет оптимизировать процесс, так как компилятор может видеть его внутренние компоненты и, возможно, избежать ненужной обработки.
В случае переменной-члена b, возможно, вы можете использовать список инициализации, чтобы избежать двух инициализаций:
Foo(): b(arg1, arg2) {}
В общем, что получается чтобы оптимизировать, сначала заставьте его работать, только затем проведите бенчмарк, чтобы увидеть, нужно ли это сделать быстрее, и если да, то профиль, чтобы узнать, где находится горлышко бутылки.
[...] в реализации моих конструкторов Foo я вызываю конструктор Bar с некоторыми аргументами, специфичными для текущего состояния, которое я буду знать во время выполнения. Насколько я понимаю, это означает, что конструктор Bar вызывается дважды - один раз, потому что я написал "Bar b;" выше (который вызывает конструктор по умолчанию, если я правильно понимаю), и один раз, потому что я пишу что-то вроде "b = Bar(arg1, arg2,...) "в Foo:: Foo() и Foo:: Foo (const Foo& f).
У тебя есть это неверный код. C++ может сделать лучше:
Когда у вас есть член
Bar bar;
в классеFoo
, это означает, что каждый экземплярFoo
действительно будет иметь экземплярBar
с именемbar
. Этот объектbar
создается всякий раз, когда создается объектFoo
, то есть во время построения этого объектаFoo
.Вы можете управлять созданием этого
bar
подобъектаFoo
в списке инициализации конструктораFoo
:Foo::Foo(Arg1 arg1, Arg2 arg2) : bar(arg1,arg2) { }
Эта строка начинается с двоеточием сообщит компилятору, какой конструктор
Bar
вызывать, когда он создает объектFoo
, используя этот конкретный конструкторFoo
. В моем примере вызывается конструкторBar
, который принимаетArg1
иArg2
.Если вы не зададите конструктор
Bar
, который будет использоваться для создания подобъектаbar
объектаFoo
, то компилятор будет использовать конструктор по умолчаниюBar
. (Это тот, который не принимает никаких аргументов.)Если вы вызываете компилятор сгенерированный конструктор
Таким образом, вы должны заплатить за одну конструкциюFoo
(компилятор генерирует по умолчанию и конструкторы копирования для вас при определенных обстоятельствах), то компилятор будет выбрать соответствующийBar
конструктор:Foo
'с конструктор по умолчанию будет вызыватьBar
'с конструктор по умолчанию,Foo
'с конструктор копирования будет вызыватьBar
с конструктор копирования. Если вы хотите переопределить эти значения по умолчанию, вы должны явно написать этиFoo
конструкторы самостоятельно (вместо того, чтобы полагаться на созданные компилятором), давая им список инициализации, в котором вызывается правый конструкторBar
.Bar
для каждой конструкцииFoo
. Если только несколько объектовFoo
не могут совместно использовать один и тот же объектBar
(используя copy-on-write, flyweight или что-то подобное), это то, что вы должны заплатить.(на стороне-Примечание: то же самое касается назначения. IME, однако, гораздо реже, что назначение должно отклоняться от поведения по умолчанию.)
Если вы используете
new bar
используйте auto_ptr - до тех пор, пока бар используется только в foo (в противном случае boost::shared_ptr, но они могут быть медленными).Теперь о конструкторе по умолчанию... вы можете избавиться от этого, предоставив:
Список инициализаторов для конструкторов Foo , которые будут инициализировать bar.
А во-вторых, нестандартный конструктор для bar, который вы используете в списке инициализаторов.
Однако в этот момент я почувствуйте, что вы создаете гору из кротового холма. Думая таким образом в Java, вы можете сэкономить Орду процессорных циклов. Однако в C++ конструктор по умолчанию (как правило) не означает больших накладных расходов.
Да, вы можете использовать элемент указателя и создать экземпляр следующим образом:
b = new Bar(arg1, arg2, ...)
. Именно это я и сделал бы, основываясь на вашем описании. Просто не забудьте удалить его (вероятно, в деструкторе Foo):delete b;
, или вы его утечете.Если вы создаете его в конструкторе Foo, и вы знаете аргументы, вы можете сохранить свой член как есть и вызвать конструктор явно в списке инициализаторов и сделать это только один раз:
Foo() : b(arg1, arg2, ...) {}
. Тогда вам не придется звонить в службу удаления.