我最近从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 idiom。有了它,如果我理解正确,我的代码将如下所示:
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方法有更好的记录?我错过了什么吗?
答案 0 :(得分:12)
我可以想到一些差异:
使用虚拟基类,您可以打破一些人们对行为良好的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 :(得分:1)
实际上,这只是一个设计决策。即使你做出了“错误的”决定,也不难改变。
pimpl还用于在堆栈上提供ligthweight对象或通过引用相同的实现对象来呈现“副本”。
委托函数可能很麻烦,但它是一个小问题(简单,所以没有真正增加的复杂性),特别是对于有限的类。
C ++中的接口通常更多地用于类似策略的方式,您希望能够选择实现,尽管这不是必需的。