18:使用std::unique_ptr管理具备专属所有权的资源

  • std::unique_ptr表示独占所有权,因此无法拷贝(拷贝构造、拷贝赋值是delete的),只能进行移动操作从而转移资源控制权
    • 例外:可以从函数返回一个std::unique_ptr
      1
      2
      3
      4
      5
      
      std::unique_ptr<int> func(int x){
          auto delInt = [&](int* p) { cout<<"My deleter"<<endl; delete p;}
          std::unique_ptr<int, decltype(delInt)> pInt(new int(x), delInt);
          return pInt;
      }
      
  • 删除器是std::unique_ptr类型的一部分
    • 在不定义删除器的情况下,std::unique_ptr内存占用和原始指针相同
    • 如果自定义删除器,则std::unique_ptr内存占用会变大
  • std::unique_ptr可以指向数组,默认删除器为delete[]std::unique_ptr<int[]> p(new int[5]{1,2,3,4,5});
    • 但是数组形式用到的场合很少,尽量使用STL
  • std::unique_ptr可以直接隐式转换为std::shared_ptr
  • 典型应用:针对继承体系,作为工厂函数的返回值类型
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    class Animal {};
    class Dog: public Animal {};
    class Cat: public Animal {};
    template <typename... Ts>
    auto makeAnimal(AnimalType type, Ts&&... AnimalInfo){ // C++14中函数返回值可以写为auto,因此unique_ptr的删除器可以放在函数内部,否则显式写出返回类型时需要知道删除器类型,因此删除器只能写在函数外部
        auto delAnimal = [](AnimalType* ptr) { makeMyLog(ptr); delete ptr; }
        std::unique_ptr<Animal, decltype(delAnimal)> up(nullptr, delAnimal);
        if(type == Dog) up.reset(new Dog(std::forward<Ts>(AnimalInfo)...)); // 参数是万能引用,这里进行完美转发
        if(type == Cat) up.reset(new Cat(std::forward<Ts>(AnimalInfo)...)); // 使用reset使得指针独占资源的所有权,不能直接将原始指针赋值给智能指针
        return up; // 返回unique_ptr
    }
    
  • 参考

19:使用std::shared_ptr管理具备共享所有权的资源

  • std::shared_ptr可以拷贝,通过引用计数来管理资源的生命周期
  • std::shared_ptr内存模型
    • 一个std::shared_ptr大小通常为普通指针的两倍:一个指针指向资源,另一个指针指向控制块
    • 控制块中通过原子操作维护引用计数,保存deleter(因此deleter不属于std::shared_ptr类型的一部分),保存弱计数等
  • std::shared_ptr的使用:
    • 使用std::make_sharedstd::unique_ptr、原始指针创建std::shared_ptr,会为资源创建一个控制块
      • 如果资源有多个控制块,就会被多次析构,因此尽量避免使用原始指针构造std::shared_ptr
    • 使用std::shared_ptrstd::weak_ptr创建一个std::shared_ptr,不会创建一个新的控制块
    • this的陷阱:
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      vector<shared_ptr<Animal>> eatList; // 追踪哪些Animal调用了eat方法
      struct Animal{
          virtual void eat(){
              eatList.emplace_back(this); // eatList.push_back(shared_ptr<Animal>(this));
          }
      };
      struct Cat: public Animal{};
      struct Dog: public Animai{};
      
      shared_ptr<Animal> myCat(new Cat);
      myCat->eat(); // 针对同一个对象创建了两个控制块
      
      • 解决方法一:使类继承自std::enable_shared_from_this,类内部使用shared_from_this方法,搜索当前对象的控制块,如果有就不用创建控制块了,如果没有则抛出异常,因此适合于当前对象已经创建过控制块的情况
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        
        vector<shared_ptr<Animal>> eatList; // 追踪哪些Animal调用了eat方法
        struct Animal: public std::enable_shared_from_this<Animal>{
            virtual void eat(){
                eatList.emplace_back(shared_from_this()); // eatList.push_back(shared_ptr<Animal>(shared_from_this()));
            }
        };
        struct Cat: public Animal{};
        struct Dog: public Animai{};
        
        shared_ptr<Animal> myCat(new Cat);
        myCat->eat(); 
        
      • 解决方法二:见127页,代码是自己实现的,有误
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        
        vector<shared_ptr<Animal>> eatList; // 追踪哪些Animal调用了eat方法
        struct Animal: public std::enable_shared_from_this<Animal>{
            public:
                template <typename... Ts>
                static shared_ptr<Animal> create(Ts&&... params) { return shared_ptr<Animal>(Animal(std::foward<Ts>(params)...)); }
                virtual void eat(){
                    eatList.emplace_back(shared_from_this()); // eatList.push_back(shared_ptr<Animal>(shared_from_this()));
                }
            private:
                Animal() {} // 构造函数
        };
        struct Cat: public Animal{};
        struct Dog: public Animai{};
        
  • 参考

20:对于类似std::shared_ptr但是可能空悬的指针使用std::weak_ptr

  • std::weak_ptr通常视为std::shared_ptr的辅助工具,通过std::shared_ptr构造std::weak_ptr
    • std::weak_ptr不会影响对象的引用计数
    • 但是std::weak_ptr没有解引用操作,必须调用lock转换为std::shared_ptr来访问对象
      • 例子:if(shared_ptr<int> p = wp.lock()> cout<<*p<<endl;
  • 典型应用:
    • 避免shared_ptr循环引用:将其中一个shared_ptr改为weak_ptr
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      struct A{
          std::shared_ptr<B> pb; // std::weak_ptr<B> pb;
      };
      struct B{
          std::shared_ptr<A> pa; 
      };
      std::shared_ptr<A> pa = std::make_shared<A>();
      std::shared_ptr<B> pb = std::make_shared<B>();
      pa->pb = pb; // pb和pa->pb同时指向同一个对象B,引用计数为2
      pb->pa = pa;
      
    • 带缓存的工厂方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      std::shared_ptr<const Widget> fastLoadWidget(WidgetID id){
          static std::unordered_map<WidgetID, weak_ptr<const Widget>>  cache;
          std::shared_ptr<const Widget> widgetPtr = cache[id].lock();
          if(!widgetPtr){ // 缓存中没有
              widgetPtr = loadWidget(id); // 调用原始工厂方法创建,并加入到缓存中
              cache[id] = widgetPtr;
          }
          return widgetPtr;
      }
      
    • 观察者设计模式:多个观察者(observer)对象同时监听一个主题(subject)对象,主题对象会在其发生状态改变时发出通知。主题对象不会控制其观察者的生存期,但需要确认当一个观察者对象被析构后,主题对象不会再访问它。一种合理的设计就是让每个主题对象持有指向其观察者对象的std::weak_ptr,以便在使用之前确认它是否空悬。
  • 参考:

21:优先选用std::make_unqiuestd::make_shared,而非直接使用new

  • make函数可以传入任意集合的参数,然后完美转发给构造函数,并动态创建一个对象,返回智能指针
    • 支持auto
    • 避免异常:将[[ch03-资源管理#17:以独立语句将new的对象置入智能指针| effective C++ item17:以独立语句将new的对象置入智能指针]]改进为使用make函数
      1
      2
      3
      4
      5
      6
      7
      
      void func(shared_ptr<Widget> sp, int priority);
      void func(shared_ptr<Widget>(new Widget), priority); // 可能由于异常导致内存泄露
      void func(make_shared<Widget>(), priority); // 不会由于异常导致内存泄露
      
      // 如果需要自定义删除器,并且又可以避免异常
      shared_ptr<Widget> sp(new Widget, myDeleter);
      func(std::move(sp), priority); // 直接传递一个右值,避免了修改引用计数
      
  • 效率更高:make函数只需要申请一次内存(同时存储对象和控制块),但是使用shared_ptr<Widget>(new Widget)需要申请两次内存(一次对象,一次控制块)
  • make函数的缺点:
    • 无法自定义deleter
    • 语义歧义:比如使用(){}初始化vector代表不同的方式,make函数可以完美转发(),不支持完美转发{}
      1
      2
      3
      4
      
      auto sp1 = make_shared<vector<int>>(2,3); // {3,3};
      shared_ptr<vector<int>> sp2(new vector{1,2,3,4,5});
      auto initList = {1,2,3,4,5};
      auto sp3 = make_shared<vector<int>>(initList); // 不支持:make_shared<vector<int>>({1,2,3,4,5});
      
    • 不建议对自定义内存管理方式的类使用 make 函数:通常情况下,类自定义的operator newoperator delete被设计成用来分配和释放能精确容纳该类大小的内存块,但std::allocate_shared所要求的内存大小并不等于动态分配对象的大小,而是在其基础上加上控制块的大小。
    • 若存在非常大的对象和比相应的std::shared_ptr生存期更久的std::weak_ptr,不建议使用 make 函数,会导致对象的析构和内存的释放之间产生延迟
      • 如果只申请一块内存(make函数),如果后来shared_ptr的引用计数为0,但是weak_ptr的引用计数不为0时,对象销毁会被延长,只有当weak_ptr的引用计数为0时,控制块才被释放
      • 如果使用new的话,可以立即销毁对象
  • 参考

22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中

  • PImpl技术(Pointer to Implementation,编译防火墙):将类的实现放在另一个单独的类中,并通过不透明的指针进行访问。因此可以有效减少编译依赖。
    • 原理:一个只声明但是不定义的类型是不完整类型,声明指向它的指针是可以通过编译的
  • 常见错误:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    // in "widget.h"
    #include <memory>
    class Widget {
        public:
            Widget();
        private:
            struct Impl;                 
            std::unique_ptr<Impl> pImpl;  
    };
    
    //==================================================================================//
    // in "widget.cpp"
    #include "widget.h"  
    #include <string>
    struct Widget::Impl { 
        std::string name;
    };
    Widget::Widget(): pImpl(std::make_unique<Impl>()){}
    
    //==================================================================================//
    // in "main.cpp"
    #include "widget.h"
    int main(){
        Widget w; // 报错:/usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘Widget::Impl’
        return 0;
    }
    
    • 报错原因:在析构Widget w时,此时看到的Impl是不完整类型
      • 在编译widget.cpp时没有问题:g++ -c widget.cpp -o widget.o
      • 在编译main.cpp时出问题:g++ -c main.cpp -o main.o
        • 没有定义Widget的析构函数,因此使用自动生成的析构函数(默认是inline的)
        • 本来如果声明了Widget的析构函数,编译时无法进行处理,后面链接时链接到定义,运行时才能析构pImpl(因为经过链接,此时也知道Impl是完整类型)
        • 但是正因为自动生成的析构函数是inline的,编译时就可以展开,此时析构pImpl当然看到的Impl是不完整类型(还没有链接到widget.o
  • 使用说明
    • 考虑到如上报错和[[ch03-转向现代C++#17:理解特殊成员函数的生成机制|item17:理解特殊成员函数的生成机制]],因此最好将拷贝控制成员和析构函数自定义,且声明与实现分离(防止进行内联)
    • 为了实现PImpl技术,使用unique_ptr是最合适的,因为pImpl指针独享Impl的所有权,如果使用shared_ptr则上述报错不会出现(因为删除器不属于类型的一部分,属于控制块,不会包含删除器的代码)
  • 参考
  • 实例
    • widget.h
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      #include <memory>
      class Widget{
      public:
          Widget(std::string s);
          Widget(const Widget& rhs);
          Widget& operator=(const Widget& rhs);
          Widget(Widget&& rhs);
          Widget& operator=(Widget&&);
          ~Widget();
      
          std::string getName() const;
      private:
          // std::string _name;
          struct Impl;
          std::unique_ptr<Impl> pImpl;
      };
      
    • widget.cpp
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      #include "widget.h"
      #include <iostream>
      #include <string>
      
      struct Widget::Impl{
          Impl(std::string name): _name(name) {};
          std::string _name;
          std::string getName() const {return _name;}
      };
      
      Widget::Widget(std::string s): pImpl(std::make_unique<Impl>(s)) {}
      Widget::~Widget() {} 
      Widget::Widget(const Widget& rhs): pImpl(std::make_unique<Impl>(*rhs.pImpl)) {}
      Widget& Widget::operator=(const Widget& rhs){
          *pImpl = *rhs.pImpl;
          return *this;
      }
      Widget::Widget(Widget&& rhs) =default;
      Widget& Widget::operator=(Widget&& rhs) =default;
      
      std::string Widget::getName() const { return pImpl->getName();}
      
    • main.cpp
      1
      2
      3
      4
      5
      6
      7
      8
      
      #include "widget.h"
      #include "iostream"
      
      int main(){
          Widget w("zhang");
          std::cout<<w.getName()<<std::endl;
          return 0;
      }