unique_ptr使用不当导致Pimpl惯用法产生坏味道

什么是Pimpl惯用法

Pimpl是一种C++编码技巧,凭借这样一种技巧,你可以将类中的数据成员换成一个指向包含具体实现类(或结构体)的指针,并将主类中的数据成员移到实现类中,通过指针间接访问这些数据成员,这样的目的是通过减少在类实现和类使用者之间的编译依赖来减少编译时间(是前向声明很好的实践)。

接下来我们编写一个简单的Person类来了解Pimpl惯用法书写的细节。

代码展示

Person.hpp

#pragma once
#include <memory>
class Person
{
public:
    Person();
private:
    class PersonImpl;
    std::unique_ptr<PersonImpl> personImplPtr;
};

L8前向声明了PersonImpl类型,只有一个指向PersonImplunique_ptr类型的成员变量personImplPtr
Person.cpp

#include "Person.hpp"
#include <string>
#include <vector>
#include <unordered_map>
// ------PersonImpl------
class Person::PersonImpl
{
public:
    PersonImpl()
    : name{"zhangsan"}, id{20}, sex{'M'}, score{{"math", 90.0}, {"english", 78.0}} {}
private:
    const std::string name;
    int id;
    char sex;
    std::unordered_map<std::string, double> score;
};

// ------Person------
Person::Person(): personImplPtr{std::make_unique<PersonImpl>()}
{
}

在源文件中首先我们定义了Person::PersonImpl类型,其中包含多种类型的成员变量,然后在Person::Person()中用std::make_unique实例化。
main.cpp

#include "Person.hpp"

int main(int argc, char*argv[])
{
    Person p;
    return 0;
}

main()中我们构造了一个普通的Person类型的变量,看起来人畜无害,但是当我们编译源文件时,则会出现以下类似错误:

In file included from /usr/include/c++/11/memory:76,
                 from Person.hpp:2,
                 from main.cc:1:
/usr/include/c++/11/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Person::PersonImpl]’:
/usr/include/c++/11/bits/unique_ptr.h:361:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Person::PersonImpl; _Dp = std::default_delete<Person::PersonImpl>]’
Person.hpp:3:7:   required from here
/usr/include/c++/11/bits/unique_ptr.h:83:23: error: invalid application of ‘sizeof’ to incomplete type ‘Person::PersonImpl’
   83 |         static_assert(sizeof(_Tp)>0,
      |                       ^~~~~~~~~~~

错误分析

大致就是说你使用的Person::PersonImpl类型是不完整的,导致unique_ptr.h:83:23断言失败,程序退出,让我们看看L83附近的代码:
unique_ptr.h:L62~L83

template<typename _Tp>
    struct default_delete
    {
      /// Default constructor
      constexpr default_delete() noexcept = default;

      /** @brief Converting constructor.
       *
       * Allows conversion from a deleter for objects of another type, `_Up`,
       * only if `_Up*` is convertible to `_Tp*`.
       */
      template<typename _Up,
	       typename = _Require<is_convertible<_Up*, _Tp*>>>
        default_delete(const default_delete<_Up>&) noexcept { }

      /// Calls `delete __ptr`
      void
      operator()(_Tp* __ptr) const
      {
	static_assert(!is_void<_Tp>::value,
		      "can't delete pointer to incomplete type");
	static_assert(sizeof(_Tp)>0,
		      "can't delete pointer to incomplete type");
	delete __ptr;
      }
    };

这是一个叫做default_delete的模板类,重载了调用运算符,这是被当作unique_ptr的默认删除器来使用的,在其中对_Tp类型进行大小判断,_Tp类型及就是我们要构造的Person::PersonImpl类型,这里为什么会失败呢?其实是由析构函数引起的。当main()中定义的变量p离开作用域准备析构时,会调用自己的析构函数,但是Person类型中并没有显示定义析构函数,所以编译器会生成一个默认版本的析构函数,且通常是inline的。在析构函数中会依次调用各个成员变量的析构函数,这里会调用std::unique_ptr的析构函数,这个函数中会调用上述的default_delete来对所申请的资源进行delete,但在delete之前会static_assert,所以就出现了上述的错误。

解决方案

Person.hpp中显示声明析构函数,在Person.cpp中定义析构函数(+代表新添加的行)。移动构造移动赋值同理。
Person.hpp

#pragma once
#include <memory>
class Person
{
public:
    Person();
    Person(Person&& rhs); // +
    Person& operator=(Person&& rhs); // +
    ~Person(); // +
private:
    class PersonImpl;
    std::unique_ptr<PersonImpl> personImplPtr;
};

Person.cpp

#include "Person.hpp"
#include <string>
#include <vector>
#include <iostream>
#include <unordered_map>
// ------PersonImpl------
class Person::PersonImpl
{
public:
    PersonImpl()
    : name{"zhangsan"}, id{20}, sex{'M'}, score{{"math", 90.0}, {"english", 78.0}} 
    {std::cout << "Person::PersonImpl()" << std::endl;}
    ~PersonImpl(){std::cout << "Person::~PersonImpl()" << std::endl;} // +
private:
    const std::string name;
    int id;
    char sex;
    std::unordered_map<std::string, double> score;
};

// ------Person------
Person::Person()
: personImplPtr{std::make_unique<PersonImpl>()} {std::cout << "Person()" << std::endl;}

Person::Person(Person&& rhs)
: personImplPtr{std::move(rhs.personImplPtr)} {std::cout << "Person(Person&&)" << std::endl;} // +

Person& Person::operator=(Person&& rhs) // +
{ // +
    std::cout << "Person& operator(Person&&)" << std::endl; // +
    personImplPtr = std::move(rhs.personImplPtr); // +
    return *this; // +
} // +

Person::~Person() {std::cout << "~Person()" << std::endl;} // +

main.cpp

#include "Person.hpp"

int main(int argc, char*argv[])
{
    Person p;
    Person p1{std::move(p)}; // + 移动构造
    Person p2; // +
    p2 = std::move(p1); // + 移动赋值
    return 0;
}

这样的话就不会报错,输出结果如下:

Person::PersonImpl()        # p.impl
Person()                    # p本身
Person(Person&&)            # p1移动构造  
Person::PersonImpl()        # p2.impl
Person()                    # p2本身
Person& operator(Person&&)  # p2移动赋值
Person::~PersonImpl()       # p2.impl析构
~Person()                   # p2本身析构
Person::~PersonImpl()       # p1.impl析构
~Person()                   # p1本身析构
~Person()                   # p析构

总结

  • Pimpl惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
  • 对于std::unique_ptr类型的pImpl指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。养成良好的编程习惯,不依赖编译器为你生成任何函数,就算不用这个函数,也要在头文件中显示声明,源文件中做空实现
  • 以上的建议只适用于std::unique_ptr,不适用于std::shared_ptr

参考文献

https://cntransgroup.github.io/EffectiveModernCppChinese/3.MovingToModernCpp/item17.html
https://cntransgroup.github.io/EffectiveModernCppChinese/4.SmartPointers/item22.html

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
unique_ptr 是 C++11 引入的智能指针之一,用于管理动态分配的对象。它的使用场景主要有以下几个方面: 1. 独占资源管理:unique_ptr 的最大特点是独占所指向的对象,即同一时间只能有一个 unique_ptr 指向一个对象。这种独占性使得 unique_ptr 在资源管理方面非常有用,例如在函数返回时,可以将对象的所有权从一个 unique_ptr 转移给另一个 unique_ptr,从而避免了手动释放资源的麻烦和潜在的内存泄漏。 2. 防止内存泄漏:由于 unique_ptr 的特性,可以保证当 unique_ptr 超出作用域时,它所管理的对象会自动被销毁。这避免了手动释放资源的繁琐和容易出错的问题,有效地防止了内存泄漏。 3. 与 RAII(资源获取即初始化)原则结合:RAII 是一种 C++ 资源管理的编程范式,unique_ptr 与之紧密结合。通过将资源(如动态分配的对象、文件句柄等)的所有权交给 unique_ptr,可以保证在任何情况下都能正确释放资源,无论是正常执行还是异常情况。 4. 支持自定义删除器:unique_ptr 允许为其管理的对象指定自定义的删除器,即在对象销毁时执行特定的清理操作。这在需要特殊资源管理行为的情况下非常有用,例如通过自定义删除器可以使用 delete[] 释放动态分配的数组。 总之,unique_ptr 提供了一种简单、安全、高效的方式来管理动态分配的对象,避免了手动资源管理所可能引发的问题,是 C++ 代码中常用的智能指针之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值