2

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);
}

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

这个版本有什么替代品会更好?

4

3 回答 3

3

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

将所有私有变量直接放入一个private节中的类中可能看起来像是将所有相关信息放在同一个地方(在类声明中),但事实证明,私有成员变量不仅不是相关信息,而且还创建了不必要的以及您的班级客户之间不需要的依赖关系以及相当于实现细节的内容。

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

正如您已经猜到的那样,有一个权衡,这可能取决于您的具体情况,也可能不重要。第一个权衡是该类将失去一些 const 正确性。您的编译器将允许您在已声明的方法中修改私有结构的内容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)
}

第二个权衡是性能之一。对于大多数应用程序,这将不是问题。但是,我遇到了需要非常频繁地操作(创建和删除)大量小对象的应用程序。在那些罕见的极端情况下,创建分配额外结构和延迟分配所需的额外处理将对您的整体性能产生影响。但是请注意,您的平均程序肯定不属于该类别,它只是在某些情况下需要考虑的事情。

于 2012-12-18T06:15:05.903 回答
2

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

这个版本有什么替代品会更好?

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

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

于 2012-12-18T06:08:06.400 回答
2

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

但我还有一些其他建议:

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

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

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

我推荐使用智能指针。换线:

WidgetPrivate* mPrivate;

std::unique_ptr<WidgetPrivate> mPrivate;

使用智能指针你不会忘记删除成员。如果在构造函数中抛出异常,已经创建的成员将始终被删除。

我的 PIMPL 变种:// widget.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);
}
于 2012-12-18T06:45:42.083 回答