我正在阅读Herb Sutter的"Exceptional C++"一书,在那本书中我学到了关于pImpl的习语.基本上,我们的想法是为a的private对象创建一个结构class并动态分配它们以减少编译时间(并且还以更好的方式隐藏私有实现).
例如:
class X
{
private:
C c;
D d;
} ;
Run Code Online (Sandbox Code Playgroud)
可以改为:
class X
{
private:
struct XImpl;
XImpl* pImpl;
};
Run Code Online (Sandbox Code Playgroud)
并且,在CPP中,定义:
struct X::XImpl
{
C c;
D d;
};
Run Code Online (Sandbox Code Playgroud)
这看起来很有趣,但我以前从未见过这种方法,既没有在我工作的公司,也没有在我看过源代码的开源项目中.所以,我想知道这种技术真的在实践中使用了吗?
我应该在任何地方使用它,还是谨慎使用?这种技术是否建议用于嵌入式系统(性能非常重要)?
背景资料:
所述PIMPL成语(指针实现)是用于执行隐藏在其中一个公共类包装的结构或类,可以不在库的公共类是的一部分外部看到的技术.
这会隐藏来自库用户的内部实现细节和数据.
在实现这个习惯用法时,为什么要将公共方法放在pimpl类而不是公共类上,因为公共类方法实现会被编译到库中,而用户只有头文件?
为了说明,此代码将Purr()实现放在impl类上并将其包装起来.
为什么不直接在公共类上实现Purr?
// header file:
class Cat {
private:
class CatImpl; // Not defined here
CatImpl *cat_; // Handle
public:
Cat(); // Constructor
~Cat(); // Destructor
// Other operations...
Purr();
};
// CPP file:
#include "cat.h"
class Cat::CatImpl {
Purr();
... // The actual implementation can be anything
};
Cat::Cat() {
cat_ = new CatImpl;
}
Cat::~Cat() {
delete cat_;
}
Cat::Purr(){ cat_->Purr(); }
CatImpl::Purr(){
printf("purrrrrr");
}
Run Code Online (Sandbox Code Playgroud) 我想知道什么会让程序员选择Pimpl习语或纯虚拟类和继承.
我知道pimpl习惯用于为每个公共方法和对象创建开销提供一个明确的额外间接.
另一方面,Pure虚拟类带有继承实现的隐式间接(vtable),我理解没有对象创建开销.
编辑:但如果你从外面创建对象,你需要一个工厂
是什么让纯虚拟类比pimpl成语更不可取?
这是我在尝试将unique_ptr用于pimpl时所看到的简化.我选择了unique_ptr,因为我真的希望类拥有指针 - 我希望pimpl指针和类的生命周期相同.
无论如何,这是标题:
#ifndef HELP
#define HELP 1
#include <memory>
class Help
{
public:
Help(int ii);
~Help() = default;
private:
class Impl;
std::unique_ptr<Impl> _M_impl;
};
#endif // HELP
Run Code Online (Sandbox Code Playgroud)
这是来源:
#include "Help.h"
class Help::Impl
{
public:
Impl(int ii)
: _M_i{ii}
{ }
private:
int _M_i;
};
Help::Help(int ii)
: _M_impl{new Help::Impl{ii}}
{ }
Run Code Online (Sandbox Code Playgroud)
我可以将它们编译成一个库就好了.但是当我尝试在测试程序中使用它时,我得到了
ed@bad-horse:~/ext_distribution$ ../bin/bin/g++ -std=c++0x -o test_help test_help.cpp Help.cpp
In file included from /home/ed/bin/lib/gcc/x86_64-unknown-linux-gnu/4.7.0/../../../../include/c++/4.7.0/memory:86:0,
from Help.h:4,
from test_help.cpp:3:
/home/ed/bin/lib/gcc/x86_64-unknown-linux-gnu/4.7.0/../../../../include/c++/4.7.0/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = …Run Code Online (Sandbox Code Playgroud) 我一直在使用pimpl成语制作一些对象,但我不确定是否使用std::shared_ptr或std::unique_ptr.
据我所知,std::unique_ptr是更有效的,但是这是没有这么多的问题,对我来说,因为这些物体是比较重量级反正做的成本std::shared_ptr比std::unique_ptr相对较小.
我目前std::shared_ptr只是因为额外的灵活性.例如,使用a std::shared_ptr允许我将这些对象存储在散列映射中以便快速访问,同时仍然能够将这些对象的副本返回给调用者(因为我相信任何迭代器或引用可能很快变得无效).
但是,这些对象确实没有被复制,因为更改会影响所有副本,所以我想知道也许使用std::shared_ptr和允许副本是某种反模式或坏事.
它是否正确?
PIMPL代表P ointer到IMPL ementation.实现代表"实现细节":类的用户不必关心的东西.
Qt自己的类实现通过使用PIMPL惯用法将接口与实现完全分开.然而,Qt提供的机制没有记录.怎么用?
我想这是关于Qt中"我如何进行PIMPL"的规范性问题.答案将由下面显示的简单坐标输入对话框界面激发.
当我们有任何半复杂的实现时,使用PIMPL的动机就变得明显了.这个问题给出了进一步的动机.即使是一个相当简单的类也必须在其界面中引入许多其他头文件.

基于PIMPL的接口非常干净且易读.
// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>
class CoordinateDialogPrivate;
class CoordinateDialog : public QDialog
{
Q_OBJECT
Q_DECLARE_PRIVATE(CoordinateDialog)
#if QT_VERSION <= QT_VERSION_CHECK(5,0,0)
Q_PRIVATE_SLOT(d_func(), void onAccepted())
#endif
QScopedPointer<CoordinateDialogPrivate> const d_ptr;
public:
CoordinateDialog(QWidget * parent = 0, Qt::WindowFlags flags = 0);
~CoordinateDialog();
QVector3D coordinates() const;
Q_SIGNAL void acceptedCoordinates(const QVector3D &);
};
Q_DECLARE_METATYPE(QVector3D)
Run Code Online (Sandbox Code Playgroud)
基于Qt 5,C++ 11的接口不需要该Q_PRIVATE_SLOT行.
将其与非PIMPL接口进行比较,该接口将实现细节隐藏在接口的私有部分中.请注意必须包含多少其他代码.
// CoordinateDialog.h
#include <QDialog>
#include <QVector3D>
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QDialogButtonBox>
class CoordinateDialog : …Run Code Online (Sandbox Code Playgroud) 关于pimpl成语有一些关于SO的问题,但我更加好奇它在实践中的使用频率.
我理解在性能和封装之间存在一些权衡,加上一些调试烦恼,因为额外的重定向.
有了这个,是应该采用每个类,还是全有或全无的方式?这是最佳做法还是个人偏好?
我意识到这有点主观,所以让我列出我的首要任务:
我总是假设我需要在某些时候将代码作为库公开,所以这也是一个考虑因素.
编辑:任何其他选项来完成相同的事情将是受欢迎的建议.
我们想在项目的某些部分使用pimpl习语.项目的这些部分也恰好是禁止动态内存分配的部分,这个决定不在我们的控制范围内.
所以我要问的是,在没有动态内存分配的情况下,有没有一种干净又好的方法来实现pimpl习语?
编辑
以下是一些其他限制:嵌入式平台,标准C++ 98,没有外部库,没有模板.
为什么make_unique调用编译?make_unqiue不要求其模板参数是完整类型吗?
struct F;
int main()
{
std::make_unique<F>();
}
struct F {};
Run Code Online (Sandbox Code Playgroud)
在从orignated问题我的"问题"与我PIMPL实现:
我确实理解为什么析构函数必须在用户声明并在实现类(PIMPL)的cpp文件中定义.
但是移动包含pimpl的类的构造函数仍会编译.
class Object
{};
class CachedObjectFactory
{
public:
CachedObjectFactory();
~CachedObjectFactory();
std::shared_ptr<Object> create(int id) const;
private:
struct CacheImpl;
std::unique_ptr<CacheImpl> pImpl;
};
Run Code Online (Sandbox Code Playgroud)
现在cpp文件:
// constructor with make_unique on incompete type ?
CachedObjectFactory::CachedObjectFactory()
: pImpl(std::make_unique<CacheImpl>())
{}
struct CachedObjectFactory::CacheImpl
{
std::map<int, std::shared_ptr<Object>> idToObjects;
};
//deferred destructor
CachedObjectFactory::~CachedObjectFactory() = default;
Run Code Online (Sandbox Code Playgroud)
有人可以解释为什么这个编译?为什么建筑和破坏之间存在差异?如果析构函数的实例化和default_deleter的实例化是一个问题,为什么make_unique的实例化不是问题?
我一直在试图解决C++ 11中的移动语义应该如何工作,而且我很难理解移动对象需要满足的条件.看看这里的答案并没有真正解决我的问题,因为无法看到如何以合理的方式将它应用于pimpl对象,尽管移动语义的参数非常适合pimpls.
我的问题最简单的说明涉及pimpl习语,如下所示:
class Foo {
std::unique_ptr<FooImpl> impl_;
public:
// Inlining FooImpl's constructors for brevity's sake; otherwise it
// defeats the point.
Foo() : impl_(new FooImpl()) {}
Foo(const Foo & rhs) : impl_(new FooImpl(*rhs.impl_)) {}
Foo(Foo && rhs) : impl_(std::move(rhs.impl_)) {}
Foo & operator=(Foo rhs)
{
std::swap(impl_, rhs.impl_);
return *this;
}
void do_stuff ()
{
impl_->do_stuff;
}
};
Run Code Online (Sandbox Code Playgroud)
现在,一旦我离开了,我该怎么办Foo?我可以安全地销毁移动的物体,我可以分配给它,这两者都绝对是至关重要的.但是,如果我尝试do_stuff使用我的Foo,它会爆炸.在我为我的定义添加移动语义之前Foo,每个人都Foo满足了它的不变性do_stuff,而现在已不再是这样了.目前似乎并没有被大量的替代品,或者说,因为(例如)将被移至距离Foo …
c++ ×10
pimpl-idiom ×10
c++11 ×3
unique-ptr ×3
oop ×2
embedded ×1
invariants ×1
qt ×1
shared-ptr ×1
templates ×1