智能指针 02

继前一篇:

shared_pr会出现循环以来造成悬挂指针。另外一个问题则是:必须确保某个对象只被一组shared pointer拥有。示例一个错误代码:

int * p = new int;
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);     //ERROR:two shared pointers manage allocated int

问题在于sp1和sp2都拥有释放权,这会导致两次delete。因此应该直接在创建对象和资源的时候即刻设置smart pointer:

shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);

这个问题可能间接发生,根据上一节的例子,准备为Person引入一个成员函数,用来建立“从kid指向parent” 的reference 及其反向reference:

shared_ptr<Person> mom(new Person(name + "'s mom"));
shared_ptr<Person> dad(new Person(name + "'s dad"));
shared_ptr<Person> kid(new Person(name, mom, dad));
kid->setParentsAndTheirKids(mom, dad);

//下面是一个错误的直观版本

class Person{
public:
    //....
    void setParentAndTheirKids(shared_ptr<Person> m = nullptr,
                               shared_ptr<Person> f = nullptr) {
         mother = m;
         father = f;
         if(m != nullptr){
            m->kids.push_back(shared_ptr<Perrson>(this));}   //ERROR
         if(f != nllptr){
            f->kids.push_back(shared_ptr<Person>(this));}    //ERROR
    //....
}
};

问题出在this的那个shared pointer 的建立。之所以那么做是为了设置mother 和 father两个成员的kids。但是为了那么做,需要一个shared pointer指向kid,而我们没有 。然而,根据this建立一个新的shared pointer不能解决问题,因为这么一来就是开启了新的拥有者团队(a new group of owners)。

对付这个问题的办法之一就是,将指向kid的那个shared pointer传递为第三实参。但c++标准库提供了另外一个选项:class std::enable_shared_from_this<>   。

    你可以从class std::enable_shared_from_this<> 派生自己的class,表现出被shared pointer管理的对象,做法是将class 名称当做template是实参传入。

    然后可以使用一个派生的成员函数 shared_from_this() 建立一个源自this的正确shared_ptr。

class Person : public  std::enable_shared_from_this<Person>{
public:
    //....
    void setParentAndTheirKids(shared_ptr<Person> m = nullptr,
                               shared_ptr<Person> f = nullptr) {
         mother = m;
         father = f;
         if(m != nullptr){
            m->kids.push_back(shared_from_this());}   //Ok
         if(f != nllptr){
            f->kids.push_back(shared_from_this());}    //OK
    //....
}
};

但shared_form_this 不能放在构造函数中,因为在Person构造结束之前,shared_ptr本身被存放在Person的基类中,也就是enable_shared_from_this<>内部的一个private成员中。所以,在构造期间,绝对无法建立shared pointer的循环引用。



 

 

get_deleter() 会取得一个pointer to deleter(如果定义deleter的话),要不就取得nullptr。只要shared pointer还拥有那个deleter,该pointer就有效。然而为了取得deleter,你必须以其类型作为template的实参,例如:

auto del = [] (int* p) {
                    delete p;
};

std::shared_ptr<int> p(new int, del);
decltype(del)* pd = std::get_deleter<decltype(del)>(p);

需要注意的是,shared pointer 并未提供release() 操作用以放弃拥有权并将对象控制权交给调用者,原因是其他shared pointer可能还拥有该对象。

 

更复杂的shared_ptr操作

aliasing 构造函数指的是“接受一个shared pointer 和一个raw pointer” ,它允许你掌握一个事实(???):某对象拥有另一对象。例如:

struct X{
    int a;
};
shared_ptr<X> px(new X);
shared_ptr<int> pi(px,&px->a);

类型X的对象拥有成员a,为了产生一个shared pointer指向a,必须保持外环对象(surrounding object)活着,做法是借由aliasing构造函数附加其reference count(被引用次数)。更复杂的例子:指向某个容器元素或者某个shared library symbol。

需要注意的是,作为一种必然结果,必须确保两对象的寿命相称(match),否则可能发生悬挂指针或者资源泄露。例如:

shared_ptr<X> sp1(new X);
shared_ptr<X> sp2(sp1,new X); //ERROR 此X永远不会被delete

sp1.reset(); //deletes firrst X:makes sp1 empty
shared_ptr<X sp3(sp1,new X); //use_count() == 0,but get() != nullptr

make_shared() 和allocate——shared() 都用来优化“被共享对象以及其相应之控制区块(如用以维护使用次数)”的创建,。注意: shared_ptr<X> (new X(...)) 执行了二次分配:一次针对X,一次针对shared pointer的控制区块(用以管理使用次数)。如果替换为make_shared<X>(...) 会快速很多,只执行一次分配而且比较安全.allocate_shared() 允许传入自己的allocator作为第一实参。

线程安全的shared pointer接口

一般而言,shared pointer并非线程安全,为了避免data race造成不明确的行为,,必须使用mutex或者lock等技术。

相当于寻常pointer之原子性C-style接口,为shared pointer设计的重载版本允许并发处理shared pointer。注意,并发访问的是pointer而非其指向的值。

std::shared_ptr<X> global;

void foo(){
    std::shared_ptr<X> local{new X};
    ...
    std::atomic_store(&global,local);
}


 



Class unique_ptr (cppreference.com)

std::unique_ptr 是通过指针占有并管理另一对象,并在 unique_ptr 离开作用域时释放该对象的智能指针。

在下列两者之一发生时用关联的删除器释放对象:

  • 销毁了管理的 unique_ptr 对象
  • 通过 operator= 或 reset() 赋值另一指针给管理的 unique_ptr 对象。

unique_ptr不允许通过赋值语法将一个寻常的指针作为初值,必须直接初始化:

    unique_ptr<int> up = new int;  //ERROR
    unique_ptr<int> up(new int);  //OK

可以调用release(),获得unique_ptr拥有的对象,于是调用者对该对象负有责任:

unique_ptr<string> up(new string("nico"));
...
string *sp = up.release();   //获得拥有权

可以通过bool()操作符检查是否unique pointer拥有对象:

if(up)
    {
        cout << *up << endl;
    }

也可以和nullptr比较,以及通过get()成员函数查询unique_ptr内的raw pointer。

转移unique_ptr的拥有权

同一个pointer不能同时作为两个unique pointer的初值。

    string* sp = new string("nico");
    unique_ptr<string> up1(sp);
    unique_ptr<string> up2(sp);     //ERROR 运行时错误

类满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 的要求,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的要求。

    unique_ptr<ClassA> up1(new ClassA);
    //copy
    unique_ptr<ClassA> up2(up1);    //ERROR not possible
    //转移所有权
    unique_ptr<ClassA> up3(std::move(up1)); //OK

因为unique_ptr是独占式的,所有up2不能成为原对象的另一个拥有者。Assignment操作符也类似。

需要注意的是,如果要指派新值给unique_ptr,新值也必须是unique_ptr,不可以是寻常pointer:

    std::unique_ptr<ClassA> ptr;
    ptr = new ClassA;       //error
    ptr = unique_ptr<ClassA>(new ClassA);  //OK 删除旧对象并拥有新对象

(上边这个乍一看跟copy很像,其实不是,因为 new ClassA是新构建的,并不是左值,故不是copy)

赋值nullptr也是可以的,和调用reset() 效率相同。

unique_ptr被当作成员

在class内使用unique_ptr可以避免资源泄露,unique_ptr可以协助避免“对象初始化期间抛出异常而造成的资源泄漏”。对于“拥有多个raw pointer”的class,如果构造期间第一个new成功而第二个失败,就可能导致资源泄漏,例如:

class ClassB {
private:
    ClassA * ptr1;
    ClassA* ptr2;
public:
    //构造函数,初始化指针,第二个new可能失败,造成资源泄漏
    ClassB(int val1, int val2) : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {}
    //复制构造函数,第二个new可能分配失败
    ClassB(const ClassB& x) :ptr1(new ClassA(*x.ptr1)),ptr2(new ClassA(*x.ptr2)){}
    //赋值操作符
    const ClassB& operator= (const ClassB& x) {
        *ptr1 = *x.ptr1;
        *ptr2 = *x.ptr2;
        return *this;
    }
    ~ClassB() {
        delete ptr1;
        delete ptr2;
    }
};

 

class ClassB {
private:
    unique_ptr<ClassA> ptr1;
    unique_ptr<ClassA> ptr2;
public:
    //构造函数,初始化unique_ptr指针,不会造成资源泄漏
    ClassB(int val1, int val2) : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {}
    //复制构造函数,
    ClassB(const ClassB& x) :ptr1(new ClassA(*x.ptr1)),ptr2(new ClassA(*x.ptr2)){}
    //赋值操作符
    const ClassB& operator= (const ClassB& x) {
        *ptr1 = *x.ptr1;
        *ptr2 = *x.ptr2;
        return *this;
    }
  //不需要析构函数 };

 

 

上边使用unique_ptr不需要写析构函数,但必须写copy构造函数和赋值操作符构造函数,因为默认版本会拷贝或者赋值成员,但这对于unique_ptr是不可能的,如果你没有自己提供,ClassB将只提供move语义。

对于Array

对于array有特殊的规定。就因为释放对象应该是delete[]而不是delete。C++ std提供了class unique_ptr的一个偏特化版本处理array:

unique_ptr<string> up(new string[10]);  //ERROR
unique_ptr<string[]> up(new string[10]);  //OK
...
cout << *up << endl;  //ERROR
cout << up[0] << endl;  //OK

注意,对于array来讲,这里* 和->不再提供,只能用操作符[]。

其他资源的dleter

当指向的资源用delete或者delete[]释放满足不了需求时,需要自定义deleter。此处的deleter定义方式不同于shared_ptr。必须指明deleter的类型作为第二个template实参。该类型可能是函数引用,或者函数指针或者函数对象(function object,也叫仿函数)。如果是函数对象,其function call操作符 () 应该接受一个 “指向对象”的pointer。举例而言:

class ClassADeleter {
public:
    void operator () (ClassA* p) {
        cout << "call delete for ClassA object" << endl;
        delete p;
    }
};

unique_ptr<ClassA, ClassADeleter> up(new ClassA);

如果你给的是个函数或lambda,你必须声明deleter的类型为 void(*)(T*) 或者 function<void(T*)>,要不就使用decltype。例如,若要为一个array of int 指定自己的deleter,并以lambda形式呈现,应该如下:

std::unique_ptr<int, void(*)(int*)> up(new int[10],
    [](int* p) {
    //...
    delete[] p;
});

std::unique_ptr<int, std::function<void(int *)>> up1(new int[10],
    [](int* p) {
    //...
    delete[] p;
});

auto l = [](int *p) {
    //...
    delete[] p;
};

std::unique_ptr<int, decltype(l)> up3(new int[10], l );

为了避免“传递function pointer 或者 lambda时必须指明deleter的类型”,可以使用 alias template这是自c11的语言特性:

template <typename T>
using uniquePtr = std::unique_ptr<T, void(*)(T*)>;
//...
uniquePtr<int> up(new int[10], [](int* p)(int* p) {
    //...
    delete[] p;
});

下面是个完整的例子 :

#include<memory>
#include<iostream>
#include<dirent.h>
#include<string>
#include<cstring>
#include<cerrno>
using namespace std;

class DirDirCloser{
public:
    void operator() (DIR* dp){
        if(closedir(dp) != 0){
            std::cerr << "Failed closedir()" << std::endl;
        }
    }
};

int main(){
    //open current dir
    unique_ptr<DIR,DirDirCloser> pDir(opendir("."));
    //...
    //process each dir entry;
    struct dirent 8 dp;
    while((dp = readdir(pDir.get())) != nullptr){
        string filename(dp->d_name);
        cout << "process " << filename << endl;
    }
}

如果必须昂处理closedir的返回值,可以直接传递closedir() 成为一个function pointer,并明确指出deleter是个function  pointer。但是务必晓得,下面这个经常被推荐的声明式子并不保证可移植:

unique_ptr<DIR, int(*)(DIR*)> pDir(opendir("."),closedir);

因为closedir具备extern “C” linkage,所以在C++代码中并不能保证被转换为int(*)(DIR*)。下面的方式具备可移植性,其需要一个中介类型定义式如下:

extern "C" typedef int(*DIRDeleter)(DIR*);
unique_ptr<DIR, DIRDeleter> pDir(open("."),closedir);

 

posted @ 2018-07-10 12:34  花园小花匠  阅读(324)  评论(0编辑  收藏  举报