std::unique_ptr使用incomplete type的报错分析和解决
Pimpl
(Pointer to implementation)很多同学都不陌生,但是从原始指针升级到C++11的独占指针std::unique_ptr
时,会遇到一个incomplete type
的报错,本文来分析一下报错的原因以及分享几种解决方法
问题现象
首先举一个传统C++中的Pimpl
的例子
// widget.h
// 预先声明
class Impl;
class Widget
{
Impl * pImpl;
};
很简单,没什么问题,但是使用的是原始指针,现在我们升级到std::unique_ptr
// widget.h
// 预先声明
class Impl;
class Widget
{
std::unique_ptr<Impl> pImpl;
};
很简单的一次升级,而且也能通过编译,看似也没问题,但当你创建一个Widget
的实例
// pimpl.cpp
#include "widget.h"
Widget w;
这时候,问题来了
$ g++ pimpl.cpp
In file included from /usr/include/c++/9/memory:80,
from widget.h:1,
from pimpl.cpp:1:
/usr/include/c++/9/bits/unique_ptr.h:
In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Impl]’:
/usr/include/c++/9/bits/unique_ptr.h:292:17:
required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Impl; _Dp = std::default_delete<Impl>]’
widget.h:5:7: required from here
/usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘Impl’
79 | static_assert(sizeof(_Tp)>0,
| ^~~~~~~~~~~
原因分析
从报错我们可以看出,std::unique_ptr
中需要静态检测类型的大小static_assert(sizeof(Impl)>0
,但是我们的Impl
是一个预先声明的类型,是incomplete type
,也就没法计算,所以导致报错
想要知道怎么解决,首先需要知道std::unique_ptr
为啥需要计算这个,我们来看一下STL中相关的源码,从报错中得知是unique_ptr.h
的292行,调用了79行,我们把前后相关源码都粘出来(来自g++ 9.3.0
中的实现)
// 292行附近
/// Destructor, invokes the deleter if the stored pointer is not null.
~unique_ptr() noexcept
{
static_assert(__is_invocable<deleter_type&, pointer>::value,
"unique_ptr's deleter must be invocable with a pointer");
auto& __ptr = _M_t._M_ptr();
if (__ptr != nullptr)
// 292行在这里
get_deleter()(std::move(__ptr));
__ptr = pointer();
}
// 79行附近
/// Primary template of default_delete, used by unique_ptr
template<typename _Tp>
struct default_delete
{
/// Default constructor
constexpr default_delete() noexcept = default;
/** @brief Converting constructor.
*
* Allows conversion from a deleter for arrays of another type, @p _Up,
* only if @p _Up* is convertible to @p _Tp*.
*/
template<typename _Up, typename = typename
enable_if<is_convertible<_Up*, _Tp*>::value>::type>
default_delete(const default_delete<_Up>&) noexcept { }
/// Calls @c delete @p __ptr
void
operator()(_Tp* __ptr) const
{
static_assert(!is_void<_Tp>::value,
"can't delete pointer to incomplete type");
// 79行在这里
static_assert(sizeof(_Tp)>0,
"can't delete pointer to incomplete type");
delete __ptr;
}
};
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
// widget.h
// 预先声明
class Impl;
class Widget
{
std::shared_ptr<Impl> pImpl;
};
改完就能通过编译了,这种改法最简单。但是缺点也很明显:使用shared_ptr
可能会改变项目的需求,shared_ptr
也会带来额外的性能开销,而且违反了“尽可能使用unique_ptr
而不是shared_ptr
”的原则(当然这个原则是我编的,哈哈)
那为什么unique_ptr
不能使用预先声明的imcomplete type
,但是shared_ptr
却可以?
因为对于unique_ptr
而言,删除器是类型的一部分:
template<typename _Tp, typename _Dp>
class unique_ptr<_Tp[], _Dp>
这里的_Tp
是element_type
,_Dp
是deleter_type
而shared_ptr
却不是这样:
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
那为什么unique_ptr
的删除器是类型的一部分,而shared_ptr
不是呢?
答案是设计如此!哈哈,说了句废话。具体来说,删除器不是类型的一部分,使得你可以对同一种类型的shared_ptr
,使用不同的自定义删除器
auto my_deleter = [](Impl * p) {...};
std::shared_ptr<Impl> w1(new Impl, my_deleter);
std::shared_ptr<Impl> w2(new Impl); // default_deleter
w1 = w2; // It's OK!
看到了么,这里的两个智能指针w1
和w2
,虽然使用了不同的删除器,但他们是同一种类型,可以相互进行赋值等等操作。而unique_ptr
却不能这么玩
auto my_deleter = [](Impl * p) {...};
std::unique_ptr<Impl, decltype(my_deleter)> w1(new Impl, my_deleter);
std::unique_ptr<Impl> w2(new Impl); // default_deleter
// w1的类型是 std::unique_ptr<Impl, lambda []void (Impl *p)->void>
// w2的类型是 std::unique_ptr<Impl, std::default_delete<Impl>>
w1 = std::move(w2); // 错误!类型不同,没有重载operator=
道理我都明白了,那为什么要让这两种智能指针有这样的区别啊?
答案还是设计如此!哈哈,具体来说unique_ptr
本身就只是对原始指针的简单封装,这样做可以提高不会带来额外的性能开销。而shared_ptr
的实现提高了灵活性,但却进一步增大了性能开销。针对不同的使用场景所以有这样的区别
方法二
自定义删除器,将delete pImpl
的操作,放到widget.cpp
源文件中
// widget.h
// 预先声明
class Impl;
class Widget
{
struct ImplDeleter final
{
constexpr ImplDeleter() noexcept = default;
void operator()(Impl *p) const;
};
std::unique_ptr<Impl, ImplDeleter> pImpl = nullptr;
};
然后在源文件widget.cpp
中
#inclued "widget.h"
#include "impl.h"
void Widget::ImplDeleter::operator()(Impl *p) const
{
delete p;
}
这种方法改起来也不复杂,但是弊端也很明显,std::make_unique
没法使用了,只能自己手动new,直接看源码吧
template<typename _Tp, typename... _Args>
inline typename _MakeUniq<_Tp>::__single_object
make_unique(_Args&&... __args)
{ return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
看出问题在哪了么?这里返回的是默认删除器类型的unique_ptr
,即std::unique_ptr<Impl, std::default_delete<Impl>>
,如方法一中所说,是不同删除器类型的unique_ptr
是没法相互赋值的,也就是说:
pImpl = std::make_unique<Impl>(); // 错误!类型不同,没有重载operator=
pImpl = std::unique_ptr<Impl, ImplDeleter>(new Impl); // 正确!每次你都要写这么一大串
当然你也可以实现一个make_impl
,并且using
一下这个很长的类型,比如:
using unique_impl = std::unique_ptr<Impl, ImplDeleter>;
template<typename... Ts>
unique_impl make_impl(Ts && ...args)
{
return unique_impl(new Impl(std::forward<Ts>(args)...));
}
// 调用
pImpl = make_impl();
看似还凑合,但总的来说,这样做还是感觉很麻烦。并且有一个很头疼的问题:make_impl
作为函数模板,没法声明和定义分离,而且其中的用到了new
,需要完整的Impl
类型。所以,你只能把这一整段实现写到源文件中,而没法在头文件中声明它们。
方法三
仅声明Widget
的析构函数,但不要在widget.h
头文件中实现它
// widget.h
// 预先声明
class Impl;
class Widget
{
Widget();
~Widget(); // 仅声明
std::unique_ptr<Impl> pImpl;
};
// widget.cpp
#include "widget.h"
#include "impl.h"
Widget::Widget()
: pImpl(nullptr)
{}
Widget::~Widget() = default; // 在这里定义
这样就解决了!是不是出乎意料的简单!并且你也可以正常的使用std::make_unique
来进行赋值。唯一的缺点就是你没法在头文件中初始化pImpl
了
但也有别的问题,因为不光是析构函数中需要析构std::unique_ptr
,还有别的也需要,比如移动构造、移动运算符等。所以在移动构造、移动运算符中,你也会遇到同样的编译错误。解决方法也很简单,同上面一样:
// widget.h
// 预先声明
class Impl;
class Widget
{
Widget();
~Widget();
Widget(Widget && rhs); // 同析构函数,仅声明
Widget& operator=(Widget&& rhs);
std::unique_ptr<Impl> pImpl;
};
// widget.cpp
#include "widget.h"
#include "impl.h"
Widget::Widget()
: pImpl(nullptr)
{}
Widget::~Widget() = default;
Widget(Widget&& rhs) = default; //在这里定义
Widget& operator=(Widget&& rhs) = default;
搞定!
参考资料
- 当使用Pimpl惯用法,请在实现文件中定义特殊成员函数 -
《Effective Modern Cpp》 - std::unique_ptr with an incomplete type won't compile - Stack Overflow
本文首发于我的个人博客,欢迎大家来逛逛~~~
原文地址:std::unique_ptr使用incomplete type的报错分析和解决 | 肝!
std::unique_ptr使用incomplete type的报错分析和解决的更多相关文章
- Dev C++编写C/C++程序 出现[Error] ld returned 1 exit status报错分析及解决
debug系列第一弹,不知道大家写程序的时候是不是都遇到过如题的报错. 我本人是经常遇到这行熟悉的令人不知所措的报错,可能是我太笨了 有时候百度无果也差不到原因,那就汇总一下目前我遇到的情况吧--持续 ...
- broken pipe 报错分析和解决办法
参考资料: 1.博客1:https://blog.csdn.net/qq_37535749/article/details/113781338 2.博客2:https://blog.csdn.net/ ...
- "XX cannot be resolved to a type "eclipse报错及解决说明
转自:http://zhaoningbo.iteye.com/blog/1137215 引言: eclipse新导入的项目经常可以看到“XX cannot be resolved to a type” ...
- "XX cannot be resolved to a type "eclipse报错及解决
好久都没有写博了,还记得自己准备考研,结果你会发现——你永远不知道,你将会走上哪个路. 长远的目标是好的,但有些时候身不由己也迫不得已!做好自己的当下就是好的. 不论搞什么,总会遇到各种各样的问题,以 ...
- mysql5.5碰到的type= MyISAM报错问题
最近把mysql升级到5.5版本,发现type= MyISAM报错,网上查了一下原来MYSQL5.5.x 版本 不支持 TYPE=MyISAM 这样的语句了!!! MYSQL语句写法 TYPE=My ...
- asp.net使用post方式action到另一个页面,在另一个页面接受form表单的值!(报错,已解决!)
原文:asp.net使用post方式action到另一个页面,在另一个页面接受form表单的值!(报错,已解决!) 我想用post的方式把一个页面表单的值,传到另一个页面.当我点击Default.as ...
- EXP导出aud$报错EXP-00008,ORA-00904 解决
主题:EXP导出aud$报错EXP-00008,ORA-00904 解决 环境:Oracle 11.2.0.4 问题:在自己的测试环境,导出sys用户下的aud$表报错. 1.故障现场 2.跟踪处理 ...
- IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式
转载请注明来源:四个空格 » IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式: 环境 ideaIU-2018.3.4.win: 错误提示: Could no ...
- Eclipse中引入com.sun.image.codec.jpeg包报错的完美解决办法
转: Eclipse中引入com.sun.image.codec.jpeg包报错的完美解决办法 更新时间:2018年02月14日 17:13:03 投稿:wdc 我要评论 Java开发中 ...
随机推荐
- PHP对接微信扫码登录
1.PC端扫码登录 如果你将微信小程序和公众账号绑定同一个微信开放平台,那么他们各自的接口返回有一个参数unionid是相同的(没有绑定微信公众账号就没有):那么你就可以使用这个unionid来做业务 ...
- STM32入门系列-STM32时钟系统,自定义系统时钟
在时钟树的讲解中我们知道,通过修改PLLMUL中的倍系数值(2-16)可以改变系统的时钟频率.在库函数中也有对时钟倍频因子配置的函数,如下: void RCC_PLLConfig(uint32_t R ...
- 16、Auth认证组件
1 Auth模块是什么 Auth模块是Django自带的用户认证模块: 我们在开发一个网站的时候,无可避免的需要设计实现网站的用户系统.此时我们需要实现包括用户注册.用户登录.用户认证.注销.修改密码 ...
- 获取url后面的参数
function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = qu ...
- 安装tomcat for ubuntu linux差点没晕死我!
我滴个神!装ubuntu的tomcat差点没有晕死我!怎么回事呢? 应该说是装好了,执行了bin/startup.sh了,出现以下画面: Using CATALINA_BASE: /home/niew ...
- JS缓冲运动案例
点击"向右"按钮,红色的#red区块开始向右缓冲运动,抵达到黑色竖线位置自动停止,再次点击"向右"#red区块也不会再运动.点击"向左"按钮 ...
- prometheus监控golang服务实践
一.prometheus基本原理介绍 prometheus是基于metric采样的监控,可以自定义监控指标,如:服务每秒请求数.请求失败数.请求执行时间等,每经过一个时间间隔,数据都会从运行的服务中流 ...
- 源码分析:同步基础框架——AbstractQueuedSynchronizer(AQS)
简介 AQS 全称是 AbstractQueuedSynchronizer,位于java.util.concurrent.locks 包下面,AQS 提供了一个基于FIFO的队列和维护了一个状态sta ...
- linux 会话 进程组 守护进程
Linux 下每个进程都会有一个非负整数表示的唯一进程 ID ,简称 pid . Linux 提供了 getpid 函数来获取 进程的 pid ,同时还提供了 getppid 函数来获取父进程的 pi ...
- 329. Longest Increasing Path in a Matrix(核心在于缓存遍历过程中的中间结果)
Given an integer matrix, find the length of the longest increasing path. From each cell, you can eit ...