std::unique_ptr使用incomplete type的报错分析和解决

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

问题现象

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

// widget.h

// 预先声明
class Impl;

class Widget
{
    Impl * pImpl;
};

很简单,没什么问题,可是使用的是原始指针,如今咱们升级到std::unique_ptrgithub

// widget.h

// 预先声明
class Impl;

class Widget
{
    std::unique_ptr<Impl> pImpl;
};

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

// pimpl.cpp

#include "widget.h"

Widget w;

这时候,问题来了app

$ 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;,仍是会出问题,由于不完整的类型没法被deleteui

解决方法

缘由已经知道了,那么解决方法就呼之欲出了,这里提供三种解决方法:设计

  • 方法一:改用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>

这里的_Tpelement_type_Dpdeleter_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!

看到了么,这里的两个智能指针w1w2,虽然使用了不一样的删除器,但他们是同一种类型,能够相互进行赋值等等操做。而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类型。因此,你只能把这一段模板函数写在源文件中,emmm,总感受不太对劲。

方法三

仅声明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;

搞定!

参考资料

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

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

相关文章
相关标签/搜索