Ark*_*nez 117 c++ abstract-class pimpl-idiom
我想知道什么会让程序员选择Pimpl习语或纯虚拟类和继承.
我知道pimpl习惯用于为每个公共方法和对象创建开销提供一个明确的额外间接.
另一方面,Pure虚拟类带有继承实现的隐式间接(vtable),我理解没有对象创建开销.
编辑:但如果你从外面创建对象,你需要一个工厂
是什么让纯虚拟类比pimpl成语更不可取?
Pau*_*rth 61
在编写C++类时,考虑它是否合适是合适的
价值类型
按值复制,身份永远不重要.它适合作为std :: map中的键.示例,"字符串"类,"日期"类或"复数"类."复制"这样一个类的实例是有道理的.
实体类型
身份很重要.总是通过引用传递,而不是"价值".通常,完全"复制"该类的实例是没有意义的.当它确实有意义时,多态"克隆"方法通常更合适.示例:Socket类,Database类,"策略"类,任何在函数式语言中都是"闭包"的东西.
pImpl和纯抽象基类都是减少编译时依赖性的技术.
但是,我只使用pImpl来实现Value类型(类型1),并且有时仅在我真正想要最小化耦合和编译时依赖性时.通常,这不值得打扰.正如您正确指出的那样,语法开销更多,因为您必须为所有公共方法编写转发方法.对于类型2类,我总是使用带有关联工厂方法的纯抽象基类.
D.S*_*ley 32
Pointer to implementation
通常是隐藏结构实现细节.Interfaces
是关于实现不同的实现.他们真的有两个不同的目的.
Pon*_*gge 27
pimpl习惯用法可以帮助您减少构建依赖性和时间,尤其是在大型应用程序中,并最大限度地减少类的实现细节到一个编译单元的标头暴露.你班上的用户甚至不需要知道疙瘩的存在(除了作为一个他们不知情的神秘指针!).
抽象类(纯虚拟)是客户必须注意的事项:如果您尝试使用它们来减少耦合和循环引用,您需要添加一些允许它们创建对象的方法(例如通过工厂方法或类,依赖注入或其他机制).
Ili*_*ini 16
我正在寻找同一个问题的答案.在阅读了一些文章和一些练习后,我更喜欢使用"纯虚拟类接口".
唯一的缺点(我试图对此进行调查)是pimpl成语可能更快
小智 9
我讨厌青春痘!他们做的课很难看,也不易读.所有方法都被重定向到疙瘩.你永远不会在标题中看到这个类有什么功能,所以你不能重构它(例如只是改变一个方法的可见性).这堂课感觉像是"怀孕".我认为使用iterfaces更好,真的足以隐藏客户端的实现.您可以让一个类实现多个接口来保持它们的精简.人们应该更喜欢接口!注意:您不需要工厂类.相关的是,类客户端通过适当的接口与其实例进行通信.私有方法的隐藏我发现是一种奇怪的偏执狂,并且因为我们有接口而没有看到这个的原因.
小智 8
共享库存在一个非常现实的问题,即pimpl习惯用尽巧妙地规避纯虚拟不能:你不能安全地修改/删除类的数据成员而不强迫类的用户重新编译它们的代码.在某些情况下这可能是可以接受的,但对于系统库则不是.
要详细解释该问题,请考虑共享库/标头中的以下代码:
// header
struct A
{
public:
A();
// more public interface, some of which uses the int below
private:
int a;
};
// library
A::A()
: a(0)
{}
Run Code Online (Sandbox Code Playgroud)
编译器在共享库中发出代码,该代码计算要初始化的整数的地址,使其成为指向它知道的A对象的指针的某个偏移量(在这种情况下可能为零,因为它是唯一的成员)this
.
在代码的用户端,a new A
将首先分配sizeof(A)
内存字节,然后将指向该内存的指针交给A::A()
构造函数as this
.
如果在库的更高版本中决定删除整数,使其更大,更小或添加成员,则用户代码分配的内存量与构造函数代码所需的偏移量之间将存在不匹配.可能的结果是崩溃,如果你很幸运 - 如果你不太幸运,你的软件表现得很奇怪.
通过pimpl'ing,您可以安全地向内部类添加和删除数据成员,因为内存分配和构造函数调用发生在共享库中:
// header
struct A
{
public:
A();
// more public interface, all of which delegates to the impl
private:
void * impl;
};
// library
A::A()
: impl(new A_impl())
{}
Run Code Online (Sandbox Code Playgroud)
您现在需要做的就是保持您的公共接口不受指向实现对象的指针之外的数据成员的影响,并且您可以避免此类错误.
编辑:我应该补充一点,我在这里谈论构造函数的唯一原因是我不想提供更多代码 - 相同的论证适用于访问数据成员的所有函数.
尽管其他答案广泛涵盖,但也许我可以更明确地说明 pimpl 相对于虚拟基类的一个好处:
从用户的角度来看,pimpl 方法是透明的,这意味着您可以在堆栈上创建类的对象并直接在容器中使用它们。如果您尝试使用抽象虚拟基类隐藏实现,则需要从工厂返回指向基类的共享指针,从而使其使用变得复杂。考虑以下等效的客户端代码:
// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();
std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
std::cout << o.SomeFun1();
// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();
std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
std::cout << o->SomeFun1();
Run Code Online (Sandbox Code Playgroud)