Я недавно переключился назад от 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 настолько лучше документируется? Я пропускал что-нибудь?
Я могу придумать несколько отличий:
С виртуальным базовым классом вы нарушаете некоторую семантику, которую люди ожидают от хорошо управляемых классов 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
).
Фактически, это всего лишь дизайнерское решение, которое нужно принять. И даже если вы примете «неправильное» решение, переключиться не так уж и сложно.
pimpl также используется для обеспечения объектов легкого веса в стеке или для представления «копий» путем ссылки на один и тот же объект реализации.
Функции делегирования могут быть проблемой, но это небольшая проблема (простая, поэтому нет реальной дополнительной сложности), особенно с ограниченными классами.
Интерфейсы в C ++ обычно больше используются стратегическими способами, когда вы ожидаете иметь возможность выбирать реализации, хотя это и не требуется.