C ++: Как улучшить производительность пользовательского класса, который будет часто копироваться? - PullRequest
4 голосов
/ 28 февраля 2010

Я переезжаю на C ++ из Java, и у меня много проблем с пониманием основ работы классов C ++ и передовых методов их проектирования. В частности, мне интересно, должен ли я использовать указатель на моего члена класса в следующем случае.

У меня есть пользовательский класс Foo, который представляет состояние игры на конкретном ходу, а Foo имеет переменную-член пользовательского класса Bar, которая представляет логическое подмножество этого игрового состояния. Например, Foo представляет шахматную доску, а Bar представляет фигуры под атакой и их ходы побега (не мой конкретный случай, а более универсальная аналогия, я думаю).

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

В Foo.h я объявляю свой класс Foo и объявляю переменную-член для него типа Bar:

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 & е).

Если я пытаюсь сделать как можно больше копий Foo в секунду, это проблема, верно?

Я думаю, что простое решение состоит в том, чтобы вместо этого объявить указатель на Bar: "Bar * b", который должен избегать создания экземпляра b дважды. Это хорошая практика? Это подводные камни, о которых я должен знать? Есть ли лучшее решение? Я не могу найти конкретный пример, чтобы помочь мне (кроме большого количества предупреждений против использования указателей), и вся информация о разработке классов действительно ошеломляющая, поэтому любые рекомендации будут с благодарностью!

РЕДАКТИРОВАТЬ: Да, у меня будет вся информация, необходимая для создания Bar, когда я создаю свой Foo. Я думаю, что все поняли это, но чтобы прояснить это, у меня уже есть нечто подобное для моего конструктора по умолчанию:

Foo(int k=5);

и в Foo.cpp:

Foo::Foo(int k) {
    b = Bar(k);
    ...
}

и затем мой Foo и его член Bar обновляются постепенно по мере изменения состояния игры.

Поэтому вызов моего пользовательского конструктора Bar в моем конструкторе Foo * объявление 1026 * список инициализации выглядит как лучший способ сделать это. Спасибо за ответы!

Ответы [ 5 ]

2 голосов
/ 28 февраля 2010

В идеале вы должны располагать всей информацией, необходимой для настройки 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=.)

2 голосов
/ 28 февраля 2010

Компиляторы очень хороши в оптимизации ненужного копирования (стандарт позволяет избегать вызовов конструктора копирования, даже если определен пользовательский copycon). Реализация коротких функций в заголовочных файлах также позволяет проводить больше оптимизаций, так как компилятор может увидеть его внутреннее и, возможно, избежать ненужной обработки.

В случае переменной-члена b, возможно, вы можете использовать список инициализации, чтобы избежать двух инициализаций:

Foo(): b(arg1, arg2) {}

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

1 голос
/ 28 февраля 2010

[...] в реализации моих конструкторов Foo я вызываю конструктор Bar с некоторыми аргументами, специфичными для текущего состояния, которые я буду знать во время выполнения. Насколько я понимаю, это означает, что конструктор Bar вызывается дважды - один раз, потому что я написал "Bar b;" выше (который вызывает конструктор по умолчанию, если я правильно понимаю), и один раз, потому что я пишу что-то вроде «b = Bar (arg1, arg2, ...)» в Foo :: Foo () и Foo :: Foo (const Foo & е).

Вы ошиблись кодом. 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 (с использованием копирования при записи, навесного веса или чего-то подобного), это то, что вы должны заплатить.

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

0 голосов
/ 28 февраля 2010

если вы используете new bar, используйте auto_ptr - до тех пор, пока bar используется только в foo (в противном случае - boost :: shared_ptr, но они могут быть медленными).

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

  1. A список инициализаторов для конструкторов Foo - который инициализирует панель.

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

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

0 голосов
/ 28 февраля 2010

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

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

...