Pimpl(Pointer to implementation)很多同学都不陌生,但是从原始指针升级到C++11的独占指针std::unique_ptr时,会遇到一个incomplete type的报错,本文来分析一下报错的原因以及分享几种解决方法

问题现象

首先举一个传统C++中的Pimpl的例子

  1. // widget.h
  2. // 预先声明
  3. class Impl;
  4. class Widget
  5. {
  6. Impl * pImpl;
  7. };

很简单,没什么问题,但是使用的是原始指针,现在我们升级到std::unique_ptr

  1. // widget.h
  2. // 预先声明
  3. class Impl;
  4. class Widget
  5. {
  6. std::unique_ptr<Impl> pImpl;
  7. };

很简单的一次升级,而且也能通过编译,看似也没问题,但当你创建一个Widget的实例

  1. // pimpl.cpp
  2. #include "widget.h"
  3. Widget w;

这时候,问题来了

  1. $ g++ pimpl.cpp
  2. In file included from /usr/include/c++/9/memory:80,
  3. from widget.h:1,
  4. from pimpl.cpp:1:
  5. /usr/include/c++/9/bits/unique_ptr.h:
  6. In instantiation of void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Impl]’:
  7. /usr/include/c++/9/bits/unique_ptr.h:292:17:
  8. required from std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Impl; _Dp = std::default_delete<Impl>]’
  9. widget.h:5:7: required from here
  10. /usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of sizeof to incomplete type Impl
  11. 79 | static_assert(sizeof(_Tp)>0,
  12. | ^~~~~~~~~~~

原因分析

从报错我们可以看出,std::unique_ptr中需要静态检测类型的大小static_assert(sizeof(Impl)>0,但是我们的Impl是一个预先声明的类型,是incomplete type,也就没法计算,所以导致报错

想要知道怎么解决,首先需要知道std::unique_ptr为啥需要计算这个,我们来看一下STL中相关的源码,从报错中得知是unique_ptr.h的292行,调用了79行,我们把前后相关源码都粘出来(来自g++ 9.3.0中的实现)

  1. // 292行附近
  2. /// Destructor, invokes the deleter if the stored pointer is not null.
  3. ~unique_ptr() noexcept
  4. {
  5. static_assert(__is_invocable<deleter_type&, pointer>::value,
  6. "unique_ptr's deleter must be invocable with a pointer");
  7. auto& __ptr = _M_t._M_ptr();
  8. if (__ptr != nullptr)
  9. // 292行在这里
  10. get_deleter()(std::move(__ptr));
  11. __ptr = pointer();
  12. }
  13. // 79行附近
  14. /// Primary template of default_delete, used by unique_ptr
  15. template<typename _Tp>
  16. struct default_delete
  17. {
  18. /// Default constructor
  19. constexpr default_delete() noexcept = default;
  20. /** @brief Converting constructor.
  21. *
  22. * Allows conversion from a deleter for arrays of another type, @p _Up,
  23. * only if @p _Up* is convertible to @p _Tp*.
  24. */
  25. template<typename _Up, typename = typename
  26. enable_if<is_convertible<_Up*, _Tp*>::value>::type>
  27. default_delete(const default_delete<_Up>&) noexcept { }
  28. /// Calls @c delete @p __ptr
  29. void
  30. operator()(_Tp* __ptr) const
  31. {
  32. static_assert(!is_void<_Tp>::value,
  33. "can't delete pointer to incomplete type");
  34. // 79行在这里
  35. static_assert(sizeof(_Tp)>0,
  36. "can't delete pointer to incomplete type");
  37. delete __ptr;
  38. }
  39. };

std::unique_ptr中的析构函数,调用了默认的删除器default_delete,而default_delete中检查了Impl,其实就算default_delete中不检查,到下一步delete __ptr;,还是会出问题,因为不完整的类型无法被delete

解决方法

原因已经知道了,那么解决方法就呼之欲出了,这里提供三种解决方法

  • 方法一:改用std::shared_ptr
  • 方法二:自定义删除器,将delete pImpl的操作,放到widget.cpp源文件中
  • 方法三:仅声明Widget的析构函数,但不要在widget.h头文件中实现它

其中我最推荐方法三,它不改变代码需求,且仅做一点最小的改动,下面依次分析

方法一

改用std::shared_ptr

  1. // widget.h
  2. // 预先声明
  3. class Impl;
  4. class Widget
  5. {
  6. std::shared_ptr<Impl> pImpl;
  7. };

改完就能通过编译了,这种改法最简单。但是缺点也很明显:使用shared_ptr可能会改变项目的需求,shared_ptr也会带来额外的性能开销,而且违反了“尽可能使用unique_ptr而不是shared_ptr”的原则(当然这个原则是我编的,哈哈)

那为什么unique_ptr不能使用预先声明的imcomplete type,但是shared_ptr却可以?

因为对于unique_ptr而言,删除器是类型的一部分:

  1. template<typename _Tp, typename _Dp>
  2. class unique_ptr<_Tp[], _Dp>

这里的_Tpelement_type_Dpdeleter_type

shared_ptr却不是这样:

  1. template<typename _Tp>
  2. class shared_ptr : public __shared_ptr<_Tp>

那为什么unique_ptr的删除器是类型的一部分,而shared_ptr不是呢?

答案是设计如此!哈哈,说了句废话。具体来说,删除器不是类型的一部分,使得你可以对同一种类型的shared_ptr,使用不同的自定义删除器

  1. auto my_deleter = [](Impl * p) {...};
  2. std::shared_ptr<Impl> w1(new Impl, my_deleter);
  3. std::shared_ptr<Impl> w2(new Impl); // default_deleter
  4. w1 = w2; // It's OK!

看到了么,这里的两个智能指针w1w2,虽然使用了不同的删除器,但他们是同一种类型,可以相互进行赋值等等操作。而unique_ptr却不能这么玩

  1. auto my_deleter = [](Impl * p) {...};
  2. std::unique_ptr<Impl, decltype(my_deleter)> w1(new Impl, my_deleter);
  3. std::unique_ptr<Impl> w2(new Impl); // default_deleter
  4. // w1的类型是 std::unique_ptr<Impl, lambda []void (Impl *p)->void>
  5. // w2的类型是 std::unique_ptr<Impl, std::default_delete<Impl>>
  6. w1 = std::move(w2); // 错误!类型不同,没有重载operator=

道理我都明白了,那为什么要让这两种智能指针有这样的区别啊?

答案还是设计如此!哈哈,具体来说unique_ptr本身就只是对原始指针的简单封装,这样做可以提高不会带来额外的性能开销。而shared_ptr的实现提高了灵活性,但却进一步增大了性能开销。针对不同的使用场景所以有这样的区别

方法二

自定义删除器,将delete pImpl的操作,放到widget.cpp源文件中

  1. // widget.h
  2. // 预先声明
  3. class Impl;
  4. class Widget
  5. {
  6. struct ImplDeleter final
  7. {
  8. constexpr ImplDeleter() noexcept = default;
  9. void operator()(Impl *p) const;
  10. };
  11. std::unique_ptr<Impl, ImplDeleter> pImpl = nullptr;
  12. };

然后在源文件widget.cpp

  1. #inclued "widget.h"
  2. #include "impl.h"
  3. void Widget::ImplDeleter::operator()(Impl *p) const
  4. {
  5. delete p;
  6. }

这种方法改起来也不复杂,但是弊端也很明显,std::make_unique没法使用了,只能自己手动new,直接看源码吧

  1. template<typename _Tp, typename... _Args>
  2. inline typename _MakeUniq<_Tp>::__single_object
  3. make_unique(_Args&&... __args)
  4. { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }

看出问题在哪了么?这里返回的是默认删除器类型的unique_ptr,即std::unique_ptr<Impl, std::default_delete<Impl>>,如方法一中所说,是不同删除器类型的unique_ptr是没法相互赋值的,也就是说:

  1. pImpl = std::make_unique<Impl>(); // 错误!类型不同,没有重载operator=
  2. pImpl = std::unique_ptr<Impl, ImplDeleter>(new Impl); // 正确!每次你都要写这么一大串

当然你也可以实现一个make_impl,并且using一下这个很长的类型,比如:

  1. using unique_impl = std::unique_ptr<Impl, ImplDeleter>;
  2. template<typename... Ts>
  3. unique_impl make_impl(Ts && ...args)
  4. {
  5. return unique_impl(new Impl(std::forward<Ts>(args)...));
  6. }
  7. // 调用
  8. pImpl = make_impl();

看似还凑合,但总的来说,这样做还是感觉很麻烦。并且有一个很头疼的问题:make_impl作为函数模板,没法声明和定义分离,而且其中的用到了new,需要完整的Impl类型。所以,你只能把这一整段实现写到源文件中,而没法在头文件中声明它们。

方法三

仅声明Widget的析构函数,但不要在widget.h头文件中实现它

  1. // widget.h
  2. // 预先声明
  3. class Impl;
  4. class Widget
  5. {
  6. Widget();
  7. ~Widget(); // 仅声明
  8. std::unique_ptr<Impl> pImpl;
  9. };
  1. // widget.cpp
  2. #include "widget.h"
  3. #include "impl.h"
  4. Widget::Widget()
  5. : pImpl(nullptr)
  6. {}
  7. Widget::~Widget() = default; // 在这里定义

这样就解决了!是不是出乎意料的简单!并且你也可以正常的使用std::make_unique来进行赋值。唯一的缺点就是你没法在头文件中初始化pImpl

但也有别的问题,因为不光是析构函数中需要析构std::unique_ptr,还有别的也需要,比如移动构造、移动运算符等。所以在移动构造、移动运算符中,你也会遇到同样的编译错误。解决方法也很简单,同上面一样:

  1. // widget.h
  2. // 预先声明
  3. class Impl;
  4. class Widget
  5. {
  6. Widget();
  7. ~Widget();
  8. Widget(Widget && rhs); // 同析构函数,仅声明
  9. Widget& operator=(Widget&& rhs);
  10. std::unique_ptr<Impl> pImpl;
  11. };
  1. // widget.cpp
  2. #include "widget.h"
  3. #include "impl.h"
  4. Widget::Widget()
  5. : pImpl(nullptr)
  6. {}
  7. Widget::~Widget() = default;
  8. Widget(Widget&& rhs) = default; //在这里定义
  9. Widget& operator=(Widget&& rhs) = default;

搞定!

参考资料

本文首发于我的个人博客,欢迎大家来逛逛~~~

原文地址:std::unique_ptr使用incomplete type的报错分析和解决 | 肝!

std::unique_ptr使用incomplete type的报错分析和解决的更多相关文章

  1. Dev C++编写C/C++程序 出现[Error] ld returned 1 exit status报错分析及解决

    debug系列第一弹,不知道大家写程序的时候是不是都遇到过如题的报错. 我本人是经常遇到这行熟悉的令人不知所措的报错,可能是我太笨了 有时候百度无果也差不到原因,那就汇总一下目前我遇到的情况吧--持续 ...

  2. broken pipe 报错分析和解决办法

    参考资料: 1.博客1:https://blog.csdn.net/qq_37535749/article/details/113781338 2.博客2:https://blog.csdn.net/ ...

  3. "XX cannot be resolved to a type "eclipse报错及解决说明

    转自:http://zhaoningbo.iteye.com/blog/1137215 引言: eclipse新导入的项目经常可以看到“XX cannot be resolved to a type” ...

  4. "XX cannot be resolved to a type "eclipse报错及解决

    好久都没有写博了,还记得自己准备考研,结果你会发现——你永远不知道,你将会走上哪个路. 长远的目标是好的,但有些时候身不由己也迫不得已!做好自己的当下就是好的. 不论搞什么,总会遇到各种各样的问题,以 ...

  5. mysql5.5碰到的type= MyISAM报错问题

    最近把mysql升级到5.5版本,发现type= MyISAM报错,网上查了一下原来MYSQL5.5.x 版本 不支持 TYPE=MyISAM  这样的语句了!!! MYSQL语句写法 TYPE=My ...

  6. asp.net使用post方式action到另一个页面,在另一个页面接受form表单的值!(报错,已解决!)

    原文:asp.net使用post方式action到另一个页面,在另一个页面接受form表单的值!(报错,已解决!) 我想用post的方式把一个页面表单的值,传到另一个页面.当我点击Default.as ...

  7. EXP导出aud$报错EXP-00008,ORA-00904 解决

    主题:EXP导出aud$报错EXP-00008,ORA-00904 解决 环境:Oracle 11.2.0.4 问题:在自己的测试环境,导出sys用户下的aud$表报错. 1.故障现场 2.跟踪处理 ...

  8. IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式

    转载请注明来源:四个空格 » IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式: 环境 ideaIU-2018.3.4.win: 错误提示: Could no ...

  9. Eclipse中引入com.sun.image.codec.jpeg包报错的完美解决办法

    转: Eclipse中引入com.sun.image.codec.jpeg包报错的完美解决办法  更新时间:2018年02月14日 17:13:03   投稿:wdc   我要评论   Java开发中 ...

随机推荐

  1. CodeForces 1067E Random Forest Rank

    题意 给定一棵 \(n\) 个节点的树,每条边有 \(\frac{1}{2}\) 的概率出现,这样会得出一个森林,求这个森林的邻接矩阵 \(A\) 的秩 \(\operatorname{rank} A ...

  2. Centos7或RedHat7下安装Mysql

    一次简单的Mysql安装记录 前言 由于网上安装Mysql的方式有很多种,但有些方式并未安装成功,比如用Yum源,还待后续查看具体是哪一步出了问题 以rpm包的形式安装Mysql 第一步:yum in ...

  3. [Luogu P3119] [USACO15JAN]草鉴定Grass Cownoisseur (缩点+图上DP)

    题面 传送门:https://www.luogu.org/problemnew/show/P3119 Solution 这题显然要先把缩点做了. 然后我们就可以考虑如何处理走反向边的问题. 像我这样的 ...

  4. 836. Rectangle Overlap ——weekly contest 85

    Rectangle Overlap A rectangle is represented as a list [x1, y1, x2, y2], where (x1, y1) are the coor ...

  5. .netcore实现jwt身份验证

    前言 http协议本身是一种无状态的协议.所以客户端的每次请求,服务端是不清楚其身份的,需要客户端每次都要将身份信息传入,服务进行验证,才能达到安全验证的目的. 传统的Web用户验证:1.客户端传入用 ...

  6. STC转STM32第一次开发

    目录 前言 项目 1. 模数转换,并通过OLED屏显示出来 需求: 实验器材: 接线: 源程序: 成品: 2. 简易频率计(0.1-10MHZ) 需求: 原理: 实验器材: 接线: 源程序: 写在结尾 ...

  7. 重拾python所要知道的一些主干知识点

    前言:因为有一段时间没有用python了,最近需要用到,只能回头过去看B站视频补一补,因为语言都是相通的,而且一些细节都可以去查表解决,所以呢,我们只需要知道一些python与其他语言的不同和常见的优 ...

  8. C# 8: 可变结构体中的只读实例成员

    在之前的文章中我们介绍了 C# 中的 只读结构体(readonly struct)[1] 和与其紧密相关的 in 参数[2]. 今天我们来讨论一下从 C# 8 开始引入的一个特性:可变结构体中的只读实 ...

  9. .NET 5 中的隐藏特性

    前言 双十一当天 .NET 5 正式发布带来了很多的新特性和改进,个人觉得非常香,并且花了 10 分钟时间就把自己的 4 个 .NET Core 3.1 的项目升级到了 .NET 5,堪称无痛. 但是 ...

  10. 在 JavaScript 中,我们能为原始类型添加一个属性或方法吗?

    原始类型的方法 JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等).JavaScript 还提供了这样的调用方法.我们很快就会学习它们,但是首先我们将了解它的工作原理,毕竟原始 ...