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

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

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

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

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

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

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

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

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

Ответы [ 2 ]

7 голосов
/ 09 ноября 2011

Типичное решение для вашего сценария будет включать объект-обработчик управления ресурсами, который вы делаете передаете по значению. Популярные кандидаты 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));. Это более строгое ограничение (поскольку в любой момент времени животное может обращаться только с одним объектом), но также и более легкий вес.

1 голос
/ 09 ноября 2011

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

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

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

void AddAnimal(Animal *animal);

Тогда вы знаете статический тип животного, но его динамический тип можно изменять, поэтому функция может принимать / any / 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_;
}

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

...