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

时间:2012-12-18 05:19:34

标签: c++

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

我宁愿不为该类的私有实现部分提供单独的头文件和源代码,因为代码基本上属于同一个类 - 如果它们不在一起呢?

此版本的哪些替代品会更好?

3 个答案:

答案 0 :(得分:3)

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

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

如果由于某种原因,您需要添加或删除私有成员变量,或者您需要更改其类型(例如从floatdouble),那么您修改了头文件它表示您的类的公共接口,并且该类的任何用户都需要重新编译。如果要在库中导出该类,则还会破坏二进制兼容性,因为除了其他方面,您可能更改了类的大小(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)
}

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

答案 1 :(得分:2)

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

  

此版本的哪些替代品会更好?

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

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

答案 2 :(得分: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);
}