хранение приватных частей вне заголовков c ++: чистый виртуальный базовый класс против pimpl - PullRequest
7 голосов
/ 22 июня 2010

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

Я быстро нашел решение, которое, я думаю, типично для программиста на 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<Impl> 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<Impl>(new Impl(name)))
{}

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

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

Итак, теперь вопрос: что не так с подходом чисто виртуального класса?Почему подход pImpl намного лучше задокументирован?Я что-то пропустил?

Ответы [ 2 ]

12 голосов
/ 22 июня 2010

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

С помощью виртуального базового класса вы нарушаете семантику, которую люди ожидают от классов 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).

1 голос
/ 22 июня 2010

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

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

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

...