如何使用unique_ptr实现pimpl?

69

这是我在尝试使用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

这里是源代码:

#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}}
{ }

我可以将它们编译成库,但当我尝试在测试程序中使用时,出现以下错误:

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 = Help::Impl]':
/home/ed/bin/lib/gcc/x86_64-unknown-linux-gnu/4.7.0/../../../../include/c++/4.7.0/bits/unique_ptr.h:245:4:   required from 'void std::unique_ptr<_Tp, _Dp>::reset(std::unique_ptr<_Tp, _Dp>::pointer) [with _Tp = Help::Impl; _Dp = std::default_delete<Help::Impl>; std::unique_ptr<_Tp, _Dp>::pointer = Help::Impl*]'
/home/ed/bin/lib/gcc/x86_64-unknown-linux-gnu/4.7.0/../../../../include/c++/4.7.0/bits/unique_ptr.h:169:32:   required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Help::Impl; _Dp = std::default_delete<Help::Impl>]'
Help.h:6:7:   required from here
/home/ed/bin/lib/gcc/x86_64-unknown-linux-gnu/4.7.0/../../../../include/c++/4.7.0/bits/unique_ptr.h:63:14: error: invalid application of 'sizeof' to incomplete type 'Help::Impl'

这是一个众所周知的安全特性。我试图遵循它。

我的问题是,如果我在头文件中放置Help::Impl声明,那么它似乎会使pimpl失去任何优势。类布局对用户可见。定义是隐藏的,但我也可以通过Help类和私有成员来实现这一点。此外,包括Impl的声明会带入新的头文件,我本来想保持分离。

我错过了什么?人们在Impl声明中放置了什么?我是否错误地处理了Help dtor?烦死了!


5
请参阅[GotW#101:编译防火墙,第2部分](http://herbsutter.com/gotw/_101/)和[此相关问题](https://dev59.com/CGoy5IYBdhLWcg3wYc8l)。 - ildjarn
1
尽管这是一个老问题,但可能需要指出的是,正如cppreference中所解释的,使用unique_ptr实现的PImpl应该被包装在类似于propagate_const的东西中以实现完全正确性。 - jdehesa
1
@jdehesa 谢谢,我正在考虑使用propagate_const来解决一些API上的尴尬问题。我几乎想知道unique_ptr在这个意义上是否默认就是有问题的。似乎propagate_const应该是内置的或者至少是默认的。 - emsr
2个回答

94

我认为你的test_help.cpp实际上看到了你声明为默认的~Help()析构函数。在该析构函数中,编译器尝试生成unique_ptr的析构函数,但是它需要Impl的声明。

所以,如果你将析构函数定义移到Help.cpp中,这个问题应该就解决了。

--编辑-- 你也可以在cpp文件中定义析构函数为默认值:

Help::~Help() = default;

10
好的,在头文件中,我刚刚声明了Help类的析构函数。然后在实现文件中,我放置了Help::~Help() = default;。太棒了!谢谢。一切正常! - emsr
4
智能指针(包括 unique_ptr)需要与不完整类型一起使用。 "删除器" 函数在构造时被分配,这样指针的用户就不需要知道它是如何被删除的。如果这解决了问题,则意味着他的 unique_ptr 实现有问题。 - edA-qa mort-ora-y
5
似乎我的评论只适用于“shared_ptr”,因此“unique_ptr”实际上是零成本的。很遗憾,没有一种等效形式的“unique_ptr”可以正确地与不完整类型配合使用。 - edA-qa mort-ora-y
8
在头文件中加入“Putting ~Help() = default;”语句使析构函数成为内联函数,这是我的障碍。 - emsr
1
我觉得有趣的是,尽管 =delete 和 =default 看起来很相似,但实际上它们非常不同。前者是一个声明,后者是一个定义。 - emsr
显示剩余3条评论

9

请注意来自 unique_ptr 定义的以下内容:

std::unique_ptr 可以为不完整类型 T 构造,例如在 pImpl 惯用法中用作句柄。如果使用默认删除器,则必须在代码中调用删除器的地方(即在 std::unique_ptr 的析构函数、移动赋值运算符和重置成员函数中)使 T 完整。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接