c++ Параметры объекта: полиморфизм, семантика значений, время жизни объекта?

При переходе с C# на C++ я получаю много рекомендаций по использованию семантики значений, где это возможно. Практически гарантировано, что если я отправлю вопрос с указателем в любом месте, кто-нибудь придет и предложит вместо этого значение. Я начинаю прозревать, и я нашел много мест в своем коде, где я мог бы заменить динамическое распределение и указатели переменными, размещенными в стеке (и обычно ссылками). Итак, я думаю, что понял, как использовать объекты, выделенные в стеке, и передавать их другим функциям в качестве ссылок, когда время жизни объекта у вызывающего больше, чем у вызываемого.

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

class Zoo
{
  void AddAnimal(Animal animal);
  std::list<Animal> animals_;
}

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

В реализации указателя клиентский код будет вызывать это так:

Animal animal = new Lion("Bob");
myZoo.AddAnimal(animal);

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

Большинство моих функций-членов, которые не принимают примитивные типы, принимают параметры абстрактного класса. Итак, каков метод С++ для решения этой проблемы? (Вот как вы программируете интерфейсы на C++ с семантикой значений?)


person User    schedule 08.11.2011    source источник
comment
Придирка: для тех, кто, наконец, понимает разницу между объектами с автоматической и динамической продолжительностью хранения (распределением стека), вы, кажется, забываете использовать указатели, когда нужно... В примерах кода Animal должно быть Animal*   -  person David Rodríguez - dribeas    schedule 09.11.2011
comment
Я не указал указатель, потому что весь мой вопрос касается семантических различий значения и указателя, и мне не хотелось печатать разные варианты. Я понимаю разницу между ними.   -  person User    schedule 09.11.2011


Ответы (2)


Типичное решение для вашего сценария включает объект обработчика управления ресурсами, который вы делаете и передаете по значению. Популярными кандидатами являются shared_ptr и unique_ptr:

#include <list>
#include <memory>
#include "all_zoo_animals.h"  // yours

typedef std::shared_ptr<Animal> AnimalPtr;  // see note
typedef std::list<AnimalPtr> AnimalCollection;

AnimalCollection zoo;

void addAnimal(AnimalPtr a)
{
  zoo.push_back(a);
}

int main()
{
  AnimalPtr a = AnimalPtr(new Penguin);
  a.feed(fish);
  addAnimal(a);  // from local variable, see note

  addAnimal(AnimalPtr(new Puffin)); // from temporary
}

Если это возможно, вы также можете определить AnimalPtr как std::unique_ptr<Animal>, но тогда вы должны сказать addAnimal(std::move(a));. Это более ограничительно (поскольку только один объект управляет животным в любой момент времени), но также легче.

person Kerrek SB    schedule 08.11.2011
comment
Если вы используете unique_ptr, внутри функции addAnimal вам придется снова использовать std::move, как в zoo.push_back(std::move(a));? Также это кажется лучшим вариантом, есть ли недостатки? - person User; 09.11.2011
comment
@User: Совершенно верное замечание. Недостатком является то, что сложнее красиво передать указатель, сохраняя его в списке. Вы могли бы использовать get() в некоторых ситуациях, но как-то странно смешивать необработанные и управляемые указатели. Вам просто нужно решить, нужна ли вам уникальная или совместная собственность. Уникальный, если возможно, общий, если необходимо. - person Kerrek SB; 09.11.2011
comment
Когда вы вызываете new Penguin, я заметил, что вы не используете круглые скобки, как в new Penguin(). Это опечатка или рекомендуемый стиль С++? - person User; 09.11.2011
comment
Не опечатка. new Penguin — это инициализация по умолчанию, а new Penguin() — инициализация значения. Вы должны использовать то, что применимо, хотя для типов классов оба вызывают конструктор по умолчанию. Однако, вполне возможно, вам все равно понадобится другой конструктор (new Penguin(Blue, "Jim", 8.7)). - person Kerrek SB; 09.11.2011
comment
Релевантно: stackoverflow.com/questions/620137/ - person R. Martinho Fernandes; 09.11.2011

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

void AddAnimal(Animal animal) { /* blah */ }

В рамках «бла» объект-животное имеет как статический, так и динамический тип Животного, что означает, что это просто Животное и только Животное. Если вместо этого вы возьмете указатель:

void AddAnimal(Animal *animal);

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

Лично я бы использовал одно из следующих трех соглашений о вызовах:

class Zoo
{
  // This object takes ownership of the pointer:
  void AddAnimal(Animal* animal);
  std::list<shared_ptr<Animal>> animals_; (or boost::ptr_list)

  // This object shares ownership with other objects:
  void AddAnimal(shared_ptr<Animal> animal);
  std::list<shared_ptr<Animal>> animals_;

  // Caller retains ownership of the pointer:
  void AddAnimal(Animal* animal);
  std::list<Animal*> animals_;
}

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

person Todd Gardner    schedule 09.11.2011
comment
Я бы серьезно посоветовал против первого подхода. Приобретение права собственности должно происходить только в очень конкретных, проверяемых и не требующих пояснений строках кода (а именно в shared_ptr конструкторах/деклараторах/присваиваниях), но никогда в каком-то непонятном вызове функции. Если вам нужны общие указатели, передайте общий указатель в качестве аргумента; если вам нужен указатель, не являющийся владельцем, передайте голый указатель (или, лучше, weak_ptr). - person Kerrek SB; 09.11.2011
comment
Мне нравится первый метод, потому что вам не нужно создавать интеллектуальный указатель только для вызова метода, но тогда, я думаю, вы теряете безопасность исключений. Кроме того, основываясь на ответе Керрека С.Б., кажется, было бы разумнее, чтобы список животных был уникальным, не так ли? - person User; 09.11.2011
comment
@Kerrek - я не большой поклонник стиля «все должно быть обернуто»; это делает вызывающий код излишне многословным, а повсеместное использование shared_ptr значительно усложняет отслеживание прав собственности. unique_ptr лучше, но не является общедоступным и по-прежнему многословным. Лично я бы просто задокументировал интерфейс. Пользователь — вы можете использовать unique_ptr, если хотите; Я не использую 0x в своем повседневном программировании. Любое из вышеперечисленного является безопасным для исключений (при условии разумной реализации); это было бы не так, если бы вы создавали более одного объекта в одном и том же списке аргументов. - person Todd Gardner; 09.11.2011
comment
@ToddGardner: Я понимаю вашу точку зрения, но если вы возьмете любой из миллиона вопросов SO C ++, которые включают некоторый стандартный контейнер T*, и спросите, почему этот сбой, вы можете более любезно отнестись к умным указателям. Что касается интерфейса вызова функции, вы, вероятно, правы в целом, но конкретно для функции addItem я думаю, что передача общего/уникального указателя - это единственная разумная вещь. - person Kerrek SB; 09.11.2011