Answers:
在编写C ++类时,考虑一下是否会
值类型
按价值复制,身份永远都不重要。使其成为std :: map中的键是适当的。例如,“字符串”类,“日期”类或“复数”类。“复制”此类的实例很有意义。
实体类型
身份很重要。始终通过引用传递,从不通过“值”传递。通常,根本根本没有“复制”该类的实例。如果确实有意义,通常使用多态“克隆”方法。示例:套接字类,数据库类,“策略”类,以及在功能语言中可能是“关闭”的任何类。
pImpl和纯抽象基类都是减少编译时间依赖性的技术。
但是,我只使用pImpl来实现Value类型(类型1),并且只有在我真的想最小化耦合和编译时依赖性时才使用。通常,这不值得打扰。正如您正确指出的那样,语法开销更大,因为您必须为所有公共方法编写转发方法。对于类型2类,我始终使用带有关联工厂方法的纯抽象基类。
Pointer to implementation
通常是关于隐藏结构实施细节。Interfaces
关于实例化不同的实现。它们确实有两个不同的目的。
我正在寻找相同问题的答案。在阅读了一些文章和一些实践之后,我更喜欢使用“纯虚拟类接口”。
唯一的缺点(我正在对此进行调查)是pimpl惯用语可能会更快
我讨厌粉刺!他们的课丑陋且不可读。所有方法都重定向到pimple。您永远不会在标头中看到该类具有什么功能,因此您无法重构它(例如,仅更改方法的可见性)。全班感觉就像“怀孕”。我认为使用iterfaces更好,而且确实足以对客户端隐藏实现。您可以通过事件让一个类实现几个接口来简化它们。一个应该喜欢接口!注意:您不需要工厂类。相关的是,类客户端通过适当的接口与其实例通信。我发现私有方法的隐藏是一种奇怪的偏执狂,因为我们有接口,所以看不到这样做的原因。
共享库有一个非常实际的问题,即pimpl惯用法巧妙地规避了纯虚拟机无法做到的事情:您不能安全地修改/删除类的数据成员,而不用强迫类的用户重新编译其代码。在某些情况下,这可能是可以接受的,但对于系统库而言,这是不可接受的。
要详细解释该问题,请考虑共享库/标题中的以下代码:
// header
struct A
{
public:
A();
// more public interface, some of which uses the int below
private:
int a;
};
// library
A::A()
: a(0)
{}
编译器在共享库中发出代码,该代码计算要初始化的整数的地址,该地址将从指向它已知为A的对象的指针偏移到某个偏移量(在这种情况下,可能是零,因为它是唯一的成员)this
。
在代码的用户端,a new A
将首先分配sizeof(A)
内存字节,然后将指向该内存的指针传递给A::A()
构造函数,如下所示:this
。
如果在您的库的更高版本中决定删除该整数,使其变大,变小或添加成员,则用户代码分配的内存量与构造函数代码期望的偏移量之间将不匹配。如果幸运的话,可能会导致崩溃-如果幸运的话,您的软件运行异常。
通过插入,您可以安全地在内部类中添加和删除数据成员,因为内存分配和构造函数调用发生在共享库中:
// 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())
{}
现在您需要做的就是使公共接口中没有指向实现对象的指针的数据成员,并且可以避免此类错误。
编辑:我也许应该补充一点,我在这里谈论构造函数的唯一原因是我不想提供更多代码-相同的论点适用于访问数据成员的所有函数。
class A_impl *impl_;
尽管在其他答案中都进行了广泛介绍,但也许我可以更加清楚地表明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();
以我的理解,这两件事有完全不同的目的。丘疹成语的目的基本上是为您提供实现的句柄,因此您可以进行诸如快速交换之类的操作。
虚拟类的用途更多地是在允许多态性的范围内,即,您有一个指向派生类型的对象的未知指针,并且当您调用函数x时,对于基本指针实际指向的任何类,您总是可以获得正确的函数。
苹果和橙子真的。
关于pimpl习惯用法,最烦人的问题是它使维护和分析现有代码变得极为困难。因此,使用pimpl时,您付出的开发人员时间和挫败感只是为了“减少构建依赖关系和时间,并使实现细节的标头暴露最小”。自己决定,是否真的值得。
尤其是“构建时间”是可以通过使用更好的硬件或使用Incredibuild(www.incredibuild.com,也已包含在Visual Studio 2017中)之类的工具解决的问题,因此不会影响您的软件设计。软件设计通常应独立于软件的构建方式。