2012-12-18 15 views
1

以下实现pImpl习语的缺点是什么?pImpl idiom - 将私有类实现放入cpp中的缺点是什么?

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

我不希望有单独的头和源类的私有实现部分是因为该代码是相同的类基本上 - 他们不应住在一起呢?

这个版本的其他选择会更好吗?

回答

2

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

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

当您需要更复杂的私有实现时,还有一种选择。例如,假设你正在使用一个你不想在头文件中公开的外部库(或者想通过条件编译进行选择),但是这个外部库很复杂,并且需要你编写一堆包装类或适配器,并且/或者您可能希望在主项目实现的不同部分以类似的方式使用该外部库。然后,你可以做的是为所有代码创建一个单独的文件夹。在那个文件夹中,你通常会创建头文件和源文件(大致为1个头文件== 1类),并且可以随意使用外部库(不需要任何东西)。然后,您的主项目中需要这些设施的部分可以仅包含在cpp文件中,并仅用于实现目的。这或多或少是任何大型包装的基本技术(例如,包装OpenGL或Direct3D调用的渲染器)。换句话说,它是类固醇的PImpl。总之,如果它只是用于外部依赖的单次使用/包装,那么你展示的技术基本上是要走的路,即保持简单。但是如果情况更复杂,那么你可以应用PImpl(编译防火墙)的原则,但是在更大比例的情况下(而不是cpp文件中的特定于ext-lib的私有类,你有一个特定于ext-lib的文件夹源文件和头文件只能在您的库/项目的主要部分中私下使用)。

+0

啊这是一个有趣的想法。我想我可以总结你的替代大包括“包括非pimpl'ed头只在你的.cpp的”。是对的吗? – UberMongoose

+0

@UberMongoose这就是它。最主要的是外部依赖不应该出现在你的库的用户将使用的任何头文件中。下面的细节实际上就是最适合你的东西。 –

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

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

+0

关于const的正确性。我想,如果我使用嵌套类作为Andrey上面提出的建议,它会使实现的私有部分保持私有,并且不允许上面提到的那种操作。它的确意味着更多的间接性,因为我必须将函数调用转发给实现。 – UberMongoose

+0

@UberMongoose:如果你正确设计你的嵌套类,它会提供*一些*正确性。但是,编译器仍然不会阻止你从主类的const方法中调用嵌套类的非const方法。因此,您仍然可以从const方法修改pImpl对象,因此违反了const正确性。 – Fred

+1

@UberMongoose通过创建类似于std :: unique_ptr的自己的RAII类型,可以修复常量正确性问题,但其中operator *()和operator - >()同时具有返回适当值的const和非const版本常量性。 –

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

嵌套课堂是一个有趣的想法。它使私人类的私人成员甚至不在封闭类中。 – UberMongoose

+0

+1指出使用'unique_ptr'。 –

相关问题