pImpl习语-将私有类实现放在cpp中有什么缺点

pImpl idiom - what are the drawbacks of putting the private class implementation in cpp?

本文关键字:cpp 缺点 什么 习语 pImpl 实现      更新时间:2023-10-16

以下实现pImpl习惯用法的缺点是什么?

// widget.hpp
// Private implementation forward declaration
class WidgetPrivate;
// Public Interface 
class Widget
{
private:
WidgetPrivate* mPrivate;
public:
Widget();
~Widget();
void SetWidth(int width);    
};
// widget.cpp
#include <some_library.hpp>
// Private Implementation
class WidgetPrivate
{
private:
friend class Widget;
SomeInternalType mInternalType;
SetWidth(int width)
{
// Do something with some_library functions
}
};
// Public Interface Implementation
Widget::Widget()
{
mPrivate = new WidgetPrivate();
}
Widget::~Widget()
{
delete mPrivate;
}
void Widget::SetWidth(int width)
{
mPrivate->SetWidth(width);
}

我不希望类的私有实现部分有单独的头和源,因为代码本质上是同一个类的——那么它们不应该放在一起吗?

这个版本的替代方案会更好吗?

首先,让我们解决私有变量是否应与类声明一起存在的问题。类声明的private部分是该类实现细节的一部分,而不是该类公开的接口。该类的任何外部"用户"(在API的情况下,无论是另一个类、另一个模块还是另一个程序)都只关心类的public部分,因为这是它唯一可以使用的东西。

将所有私有变量直接放入private节中的类中,看起来可能会将所有相关信息放在同一个位置(在类声明中),但事实证明,私有成员变量不仅不是相关信息,还会在类的客户端和实现细节之间创建不必要和不需要的依赖关系。

如果出于某种原因,需要添加或删除私有成员变量,或者需要更改其类型(例如,从float更改为double),则修改了表示类的公共接口的头文件,并且需要重新编译该类的任何用户。如果在库中导出该类,也会破坏二进制兼容性,因为您可能更改了类的大小(sizeof(Widget)现在将返回不同的值)。当使用pImpl时,您可以通过将实现细节保留在客户端看不到的位置来避免那些人为的依赖关系和兼容性问题。

正如你所猜测的,有一个权衡,根据你的具体情况,这可能很重要,也可能不重要。第一个折衷是类将失去一些常量正确性。编译器将允许您在声明为const的方法中修改私有结构的内容,而如果它是私有成员变量,则会引发错误。

struct TestPriv {
int a;
};
class Test {
public:
Test();
~Test();
void foobar() const;
private:
TestPriv *m_d;
int b;
};
Test::Test()
{
m_d = new TestPriv;
b = 0;
}
Test::~Test()
{
delete m_d;
}
void Test::foobar() const
{
m_d -> a = 5; // This is allowed even though the method is const
b = 6;        // This will not compile (which is ok)
}

第二个折衷是性能。对于大多数应用程序来说,这不会是一个问题。然而,我遇到的应用程序需要非常频繁地操作(创建和删除)大量的小对象。在这些罕见的极端情况下,创建分配额外结构和推迟分配所需的额外处理将对您的整体性能造成影响。然而,请注意,你的普通课程肯定不属于这一类,在某些情况下,这只是需要考虑的问题。

我也做同样的事情。它适用于PImpl习语的任何简单应用程序。没有严格的规则规定私有类必须在自己的头文件中声明,然后在自己的cpp文件中定义。当它是一个只与一个特定cpp文件的实现相关的私有类(或函数集)时,将声明+定义放在同一个cpp文件中是有意义的。它们合在一起是合乎逻辑的。

这个版本的替代方案会更好吗?

当您需要更复杂的私有实现时,有一种替代方案。例如,假设您使用的是一个外部库,您不想在头中公开该库(或希望通过条件编译使其成为可选库),但该外部库很复杂,需要编写一堆包装类或适配器,和/或您可能希望在主项目实现的不同部分以类似的方式使用该外部库。然后,您可以为所有代码创建一个单独的文件夹。在该文件夹中,您可以像往常一样创建头和源(大约1个头==1个类),并且可以随意使用外部库(没有任何PImpl'ing)。然后,主项目中需要这些设施的部分可以简单地将它们包含在cpp文件中并仅用于实现目的。这或多或少是任何大型包装器(例如,包装OpenGL或Direct3D调用的渲染器)的基本技术。换句话说,它是类固醇的PImpl。

总之,如果它只是用于外部依赖的单个服务使用/包装,那么您展示的技术基本上就是要走的路,即保持简单。但是,如果情况更复杂,那么您可以应用PImpl(编译防火墙)的原理,但比例更大(您有一个源文件和头文件的ext-lib特定文件夹,而不是cpp文件中特定于ext-lib的私有类,这些文件和头仅在库/项目的主要部分中私人使用)。

我认为没有太大区别。你可以选择更方便的替代方案。

但我还有一些其他建议:

通常在PIMPL中,我将实现类声明放在接口类中:

class Widget
{
private:
class WidgetPrivate;
...
};

这将防止在Widget类之外使用WidgetPrivate类。因此,您不需要将Widget声明为WidgetPrivate的好友。您还可以限制对WidgetPrivate实现细节的访问。

我建议使用智能指针。变更行:

WidgetPrivate* mPrivate;

std::unique_ptr<WidgetPrivate> mPrivate;

使用智能指针,您不会忘记删除成员。在构造函数中引发异常的情况下,已创建的成员将始终被删除。

我的PIMPL变体://小工具.hpp

// Public Interface 
class Widget
{
private:
// Private implementation forward declaration
class WidgetPrivate;
std::unique_ptr<WidgetPrivate> mPrivate;
public:
Widget();
~Widget();
void SetWidth(int width);    
};
// widget.cpp
#include <some_library.hpp>
// Private Implementation
class Widget::WidgetPrivate
{
private:
SomeInternalType mInternalType;
public:
SetWidth(int width)
{
// Do something with some_library functions
}
};
// Public Interface Implementation
Widget::Widget()
{
mPrivate.reset(new WidgetPrivate());
}
Widget::~Widget()
{
}
void Widget::SetWidth(int width)
{
mPrivate->SetWidth(width);
}