хранение половых органов вне заголовков C++: чистый виртуальный базовый класс по сравнению с pimpl

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

Я быстро предложил решение то есть, я предполагаю, типичный для программиста Java: интерфейсы (= чистые виртуальные базовые классы). Например:

BananaTree.h:

class Banana;

class BananaTree
{
public:
  virtual Banana* getBanana(std::string const& name) = 0;

  static BananaTree* create(std::string const& name);
};

BananaTree.cpp:

class BananaTreeImpl : public BananaTree
{
private:
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

public:
  BananaTreeImpl(string name) 
    : name(name)
  {}

  virtual Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }
};

BananaTree* BananaTree::create(string const& name)
{
  return new BananaTreeImpl(name);
}

Единственная стычка здесь, то, что я не могу использовать new, и должен вместо этого звонить BananaTree::create(). Я не думаю, что это - действительно проблема, тем более, что я ожидаю использовать фабрики много так или иначе.

Теперь, мудрецы известности C++, однако, предложили другое решение, pImpl идиому. С этим, если бы я понимаю это правильно, мой код был бы похож:

BananaTree.h:

class BananaTree
{
public:
  Banana* addStep(std::string const& name);

private:
  struct Impl;
  shared_ptr pimpl_;
};

BananaTree.cpp:

struct BananaTree::Impl
{
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

  Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }

  Impl(string const& name) : name(name) {}
}

BananaTree::BananaTree(string const& name)
  : pimpl_(shared_ptr(new Impl(name)))
{}

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->getBanana(name);
}

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

Так, теперь для вопроса: Что не так с чистым виртуальным подходом класса? Почему подход pImpl настолько лучше документируется? Я пропускал что-нибудь?

7
задан skrebbel 22 June 2010 в 11:02
поделиться

2 ответа

Я могу придумать несколько отличий:

С виртуальным базовым классом вы нарушаете некоторую семантику, которую люди ожидают от хорошо управляемых классов C ++:

Я бы ожидал (или даже потребовал), чтобы класс был создается в стеке, например:

BananaTree myTree("somename");

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

Я также ожидаю, что для копирования класса я могу просто сделать это

BananaTree tree2 = mytree;

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

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

Поймет ли читатель вашего кода, что BananaTree в основном не работает, что он должен использовать вместо него BananaTree * или BananaTree & ?

] По сути, ваш интерфейс не очень хорошо работает с современным C ++, где мы предпочитаем

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

Кстати, ваш виртуальный базовый класс забыл о виртуальном деструкторе. Это явная ошибка.

Наконец, более простой вариант pimpl, который я иногда использую, чтобы сократить объем шаблонного кода, - это предоставить «внешнему» объекту доступ к элементам данных внутреннего объекта, чтобы избежать дублирования интерфейса. Либо функция внешнего объекта просто получает доступ к необходимым данным от внутреннего объекта напрямую, либо вызывает вспомогательную функцию для внутреннего объекта, у которой нет эквивалента для внешнего объекта.

В вашем примере вы можете удалить функцию и Impl :: getBanana и вместо этого реализовать BananaTree :: getBanana следующим образом:

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->findBanana(name);
}

тогда вам нужно будет реализовать только один getBanana (в классе BananaTree ) и одна функция findBanana (в классе Impl ).

12
ответ дан 6 December 2019 в 19:32
поделиться

Фактически, это всего лишь дизайнерское решение, которое нужно принять. И даже если вы примете «неправильное» решение, переключиться не так уж и сложно.

pimpl также используется для обеспечения объектов легкого веса в стеке или для представления «копий» путем ссылки на один и тот же объект реализации.
Функции делегирования могут быть проблемой, но это небольшая проблема (простая, поэтому нет реальной дополнительной сложности), особенно с ограниченными классами.

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

1
ответ дан 6 December 2019 в 19:32
поделиться
Другие вопросы по тегам:

Похожие вопросы: