• RAII和unique_ptr


    RAII

    RAII是Resource Acquisition Is Initialization的缩写,是在面向对象(object-oriented)语言中使用的一种编程习惯,主要是用来在C++中处理异常安全资源管理(exception-safe resource management)。

    在RAII中,资源的获取和释放和对象的声明周期紧密联系在一起,当对象构造的时候,在构造函数中申请资源( resource allocation),而在对象的析构函数中释放资源(resource deallocation),当析构函数正确执行时,就不会有资源泄露,这让我想起了Linux内核驱动,在编写驱动的过程中,一般也是在init函数中申请资源,然后在exit函数中释放资源,能够避免资源泄露,因此这种思想普遍运用在编程中。

    优点

    RAII作为一种资源管理的技术主要有以下优点:

    1. 封装性,因为将异常处理的逻辑写到了构造和析构函数中,因此不用调用者关心怎么对资源进行管理。
    2. 异常安装,对于栈对象,当异常发生的时候,异常处理会在离开当前scope的时候,执行对象的析构函数,从而释放内存。
    3. 可定位,能够将资源申请和释放的逻辑集中写在构造和析构中,不会散乱在各个地方。

    典型应用

    RAII作为一种资源管理技术,主要运用于下面几个地方: 1. 在多线程环境下控制同步锁。在多线程中,为了线程间同步,经常要申请、释放锁,而有时候,经常会出现申请了,但是没有或者由于异常等原因没有释放锁,从而出现死锁。对于这种申请、释放锁的共走,可以交给对象的构造和析构函数,这样可以将申请、释放封装起来,并且保证了异常安全。 2. 文件处理,一般在处理文件的时候,我们都要先open-write/read-close,对于这种固定的结构,完成可以将read-write封装起来。 3. 动态对象的所有权问题。针对动态对象的所有权,C++提供了smart pointer,其中std::unique_ptr针对单拥有权问题,而std::shared_ptr则是针对共享对象。

    例子:

    #include <string>
    #include <mutex>
    #include <iostream>
    #include <fstream>
    #include <stdexcept>
    
    void write_to_file (const std::string & message) {
        // 互斥访问
        static std::mutex mutex;
    
        // 加锁
        std::lock_guard<std::mutex> lock(mutex);
    
        // 打开文件
        std::ofstream file("example.txt");
        if (!file.is_open())
            throw std::runtime_error("unable to open file");
    
        // 写
        file << message << std::endl;
    
        // 在离开scope的时候,ofstream在析构函数中关闭文件,
        // lock_guard会在构造中加锁,在析构中解锁
    }
    

    GCC对于C的扩展

    gnu针对C提供了一种非标准的扩展来支持RAII: "cleanup" variable attribute。下面是一个例子:

    static inline void fclosep(FILE **fp) { if (*fp) fclose(*fp); }
    #define _cleanup_fclose_ __attribute__((cleanup(fclosep)))
    void example_usage() {
      _cleanup_fclose_ FILE *logfile = fopen("logfile.txt", "w+");
      fputs("hello logfile!", logfile);
    }
    

    下面对前面提到的unique_ptr进行介绍。

    C++11: unique_ptr

    unique_ptr是C++11新增的一个特性,作为一种新的smart pointer,主要用于管理只有单一拥有权的动态对象。

    基本用法:

    std::unique_ptr<foo> p( new foo(42) );
    

    unique_ptr有着智能指针的优点,在析构函数中会自动释放资源。

    在C++11之前有auto_ptr用来做动态对象的管理,但是复制auto_ptr会将控制权从右值转移到左值,原先的右值将不再拥有对象的控制权,这和传统的“复制”语义不符,而且由于这种复制语义,导致了auto_ptr不能被用在标准的容器中。在C++11中出现了unique_ptr开解决这个问题。使用unique_ptr的时候,unique_ptr可以存储在容器中,当容器析构的时候unique_ptr指向的对象也被释放。

    那C++11是怎么解决不能存在在容器中的问题的呢?通过添加了rvalue reference和move语义。

    什么是rvalue reference?

    rvalue reference是C++的一个小扩展,rvalue reference允许coder避免逻辑上不必要的复制,并且提供了一个完美的转发功能。主要目的是帮助设计高性能、健壮的库。在A Brief Introduction to Rvalue References中有对rvalue reference的介绍。那为什么要引入rvalue reference,引入rvalue reference解决了什么问题呢?

    Lvalues vs rvalues

    传递给函数参数的时候,我们可以传值或者传引用。在C中,传引用是通过传递指针,而指针实际上就是传递的一个地址,在C/C++中,内存模型就是申请一个box,然后在这个box中存放数据,而传递的时候就是传递这个box的地址,对于这个box怎么管理从而引申出好多问题。但是不是所有的数据都有地址,譬如某些存放在寄存器中的数据,此时就没有地址了,因此,对于有地址的数据我们叫做lvalues,而没地址的数据叫rvalues。那实际中有哪些rvalues,譬如函数的返回值,数值计算式等。

    下面是一个例子:

    void f(int *pi);
    int g();
    
    f(&g()); //error
    f(&(1+1)); // error
    

    上面这个是无法通过的,因为传入的参数是右值,没有地址,一个简单的修改如下:

    int temp1 = g();
    f(&temp1); 
    int temp2 = 1 + 1;
    f(&temp2;
    

    那为什么编译器不帮我们创建临时变量,然后不再报错呢?

    让我们想下,一般当参数是地址的时候,我们在函数内部是希望改变指向的对象的值,如:

    void f(int *pi){ ++*pi; }
    

    此时我们如果传入一个临时变量,对于这个函数内部的操作就变的没有意义了。因此,此时编译器就会报错,不允许将rvalue的地址传入。

    Reference

    在C++中,除了指针外,还加入了引用,于是,上面的代码可以重写如下:

    void f(int &pi){ pi++; }
    int g();
    
    f(g()); //error
    f((1+1)); // error
    

    此时还是无法得到rvalue的引用(reference),但是有时候传递引用并不是为了改变其值,对于一些大的数据,传递引用可以减少开销,此时并不对lvalue和rvalue有要求。因此,编译器因该能够允许传递rvalue的reference。

    Const reference

    当我们不会改变传递进来的reference的值时,则传递进来lvalue或者rvalue都是可以的,因此,上面的代码如果改成下面的:

    void f(const int &pi){ pi++; }
    int g();
    
    f(g()); //ok
    f((1+1)); // ok
    

    此时看起来一切都挺好的,但是引入ato_ptr后,就会带来一系列的问题。

    现在总结下reference和lvalue和rvalue的关系:

    1. reference可以绑定到lvalues
    2. const reference可以绑定到lvalues和rvalues,但是不允许改变源值

    上面看上去都挺好的,除了不能将reference绑定到rvalues,并且修改,但是谁又想要改变一个临时的值呢?

    auto_ptr

    auto_ptr是智能指针中的一员,用来自动释放指向的内存块,auto_ptr有value的语义,可以传递,在栈上创建,或者是其他数据的长成员,在传递给函数的时候是通过值传递,但是当auto_ptr进行值传递的时候,其指向的数据并不复制,这又像reference的行为,同时可以对auto_ptr使用*和->,就像指针一样。

    考虑下面的代码:

    auto_ptr<int> create() {
       return auto_ptr<int>(new int(42));
     }
    
     auto_ptr<int> ap = create();
    

    考虑上面的代码,在create()内部,当结束的时候,刚离开scope的时候,如果不做什么,编译器会调用auto_ptr的析构函数,从而释放内存;函数create返回的是一个rvalue。

    对于第一点,调用析构函数,我们希望将其赋给ap,此时因该调用拷贝构造函数,而对于右值有下面的两个问题:

    1. 如果我们定义拷贝构造函数的source为const reference,则此时无法修改source,将拥有权转移。
    2. 如果定义非const的reference,此时不能够绑定到右值。

    rvalue reference

    对于上面的问题,我们需要的是rvalue reference,能够绑定到rvalue,并且修改他。于是针对auto_ptr不能复制构造右值,出现了unique_ptrunique_ptr有下面的复制构造函数:

    unique_ptr::unique_ptr(unique_ptr && src)
    

    rvalue reference能够同时绑定到lvalue和rvalue,并且不阻止改变值。

    why auto_ptr can't store auto_ptr objects in most containers

    auto_ptr还有一点问题是,不能够存储在大多数容器中,为什么呢?考虑下面的一个例子:

     auto_ptr<Foo> pSrc(new Foo);
     auto_ptr<Foo> pDest = pSrc; // 看起来像像拷贝,但是不具备拷贝的语义,会将源中指针置空    pSrc->method(); // 运行时错误,因为此时pSrc中指针为空
    

    而在容器中,会有临时变量,一旦拷贝,则容器中的值将不再有所有权,这是严重的错误。

    但是有时候我们又想要将控制权进行转移,此时我们应该明确的告知说我们现在要转移控制权了,因此不要再去使用原先的源了。

    此处我们总结下rvalue和lvalue的不同,rvalue会在赋值后消失,而lvalue则保持不变。我们看到上面的unique_ptr的复制构造函数:

    unique_ptr::unique_ptr(unique_ptr && src)
    

    这会同时绑定rvalue好lvalue,因此我们需要下面的一个重载函数:

    unique_ptr::unique_ptr(unique_ptr & src)
    

    重载rvalue,此时我们要做的就是将rvalue的构造私有化,现在unique_ptr只保持转移控制权的语义了,而当我们想要将一个lvalue转移到unique_ptr的时候,此时使用move函数,

    unique_ptr pSrc(new Foo);
    unique_ptr pDest = move(pSrc);
    

    明确的将lvalue变为rvalue,move作用就是将一个lvalue转成rvalue,

    template <class T>
     typename remove_reference<T>::type&& move(T&& t) {
        return t;
     }
    

    下面我们接着介绍unique_ptr,unique_ptr中unique在此处是指什么呢?此处unique表示,当你创建一个对象的时候,只会有一份拷贝,只会有一个指针。

    平时我们使用指针的时候,经常会碰到下面的情形:

    foo *p = new foo("useful object");
    make_use( p );
    

    首先创建一个对象,然后将指针传递给make_use,此时在make_use会对指针做什么呢?make_use会拷贝指针,稍后使用嘛?或者make_use会释放指针吗?我们无法很好的回答这些问题,因为C++不能保证make_use对指针的使用规范,我们只能通过检查代码来保证不会错误的时候指针。这些问题可以通过unique_ptr解决,只保证一份拷贝,不会有其他拷贝。

    现在我们使用指针的时候,将其放入到unique_ptr,保证拥有权是唯一的,不会隐式转移,要转移时通过明确的move函数将其转成rvalue,进行转移,此处,使用unique_ptr后,会带来的一点不同是,我们将会传递引用,将其当做值来传递,如下面:

    void inc_baz( std::unique_ptr<foo> &p )
    {
        p->baz++;
    }
    

    总结

    本文首先介绍了RAII,即Resource Acquisition Is Initialization一种资源安全管理的方式,然后引出了smart point,随后又介绍了auto_ptr,早期对RAII的一种尝试,后来由于其存在的一些问题,通过介绍lvalue、rvalue的概念,最后在C++11中给出了解决方案,unique_ptr,并给出move语义,明确的将左值转换为rvalue,进行控制权的转移。

    参考:

    http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

    http://www.drdobbs.com/cpp/c11-uniqueptr/240002708

    http://bartoszmilewski.com/2008/10/18/who-ordered-rvalue-references-part-1/

  • 相关阅读:
    意向锁
    锁升级
    使用SQL SERVER PROFILER 捕获和分析死锁
    用Go写一个聊天软件
    Js中的一个日期处理格式化函数
    javascript format 字符串 函数
    php 读取excel 时间列
    PHP发送post请求
    javascript getElementsByClassName扩展函数
    [ASP.NET] Session 详解
  • 原文地址:https://www.cnblogs.com/shengjianjun/p/3699920.html
Copyright © 2020-2023  润新知