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 4

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, но они могут быть медленными).

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

  1. Список инициализаторов для конструкторов Foo , которые будут инициализировать bar.

  2. А во-вторых, нестандартный конструктор для bar, который вы используете в списке инициализаторов.

Однако в этот момент я почувствуйте, что вы создаете гору из кротового холма. Думая таким образом в Java, вы можете сэкономить Орду процессорных циклов. Однако в C++ конструктор по умолчанию (как правило) не означает больших накладных расходов.

Да, вы можете использовать элемент указателя и создать экземпляр следующим образом: b = new Bar(arg1, arg2, ...). Именно это я и сделал бы, основываясь на вашем описании. Просто не забудьте удалить его (вероятно, в деструкторе Foo): delete b;, или вы его утечете.

Если вы создаете его в конструкторе Foo, и вы знаете аргументы, вы можете сохранить свой член как есть и вызвать конструктор явно в списке инициализаторов и сделать это только один раз: Foo() : b(arg1, arg2, ...) {}. Тогда вам не придется звонить в службу удаления.