C++中如何将RAII原则转化为?
摘要:什么是 RAII RAII(资源获取即初始化,Resource Acquisition Is Initialization),作为 C++ 的一个重要编程范式,已经被贯彻于标准库的各个角落。RAII 的核
什么是 RAII
RAII(资源获取即初始化,Resource Acquisition Is Initialization),作为 C++ 的一个重要编程范式,已经被贯彻于标准库的各个角落。RAII 的核心思想是将资源与类的生命周期绑定,RAII 类是针对内部资源封装的资源管理类。
RAII 有什么作用
RAII 的作用主要体现在:自动资源管理,异常安全,简化代码,提高可维护性。
自动资源管理 获取资源后交由 RAII 类保管,离开作用域后资源被妥善释放,减少手动资源管理容易出现的忘记释放和重复释放。
异常安全 代码可能在任何步骤抛出异常,C++ 保证在异常发生后,已经完全构造的局部变量会被析构,所以如果资源被一个已经构造好的 RAII 类保存着,那么在异常发生后它就能被安全释放。
简化代码 在复杂逻辑,特别是多返回路径的函数中,使用 RAII 类管理资源或状态,可大大降低手动管理带来的复杂性,增强可读性。
提高可维护性 RAII 类封装了资源管理的细节,与其他逻辑分离,便于代码维护。
RAII 类的工作原理
RAII 类依赖于 C++ 的栈对象生命周期管理机制,通过定义构造、拷贝和析构函数来精确控制类在创建、复制和销毁时的行为,以实现核心的资源保存、流转和释放。
构造函数 构造函数接受资源,将其存储在类中,同时初始化相关状态或接受其他与资源管理相关联的数据。比如 std::shared_ptr 除了存储指针外,还存储该指针的引用计数,在构造时必须初始化引用计数,它还支持传入自定义的删除器(我的上一篇随笔C++ 智能指针的删除器对它作过讨论)。
拷贝和移动函数 包括拷贝构造、移动构造、拷贝赋值、移动赋值四个成员函数,它们共同描述了资源的转移行为。
当资源为独占时,就不能允许发生复制动作,那么拷贝构造和拷贝赋值函数应该定义为删除,但是从一个临时的 RAII 类接管资源很合理,所以需要定义它的移动构造和移动赋值函数。一个现成的例子就是 std::unique_ptr:
代码
std::unique_ptr<int> create_unique(int value)
{
std::unique_ptr<int> ret(new int(value));
return ret;//可能触发{{tip-code}}NRVO{{/tip-code}}
}
std::unique_ptr<int> piu1(new int(42));
std::unique_ptr<int> piu2 = piu1;//错误,无法拷贝构造
std::unique_ptr<int> piu3;
piu3 = piu1;//错误,无法拷贝赋值
piu3 = create_unique(42);//可以,接管指针
piu3 = std::move(piu1);//强行转移所有权
piu3.reset(piu1.release());//使用unique_ptr提供的接口强行转移所有权
上述代码提到 NRVO(Named Return Value Optimization,具名返回值优化)是 C++ 拷贝消除机制(Copy Elision)的一种具体形式,该机制旨在消除不必要的临时对象拷贝以提高程序性能,可到 cppreference:copy_elision 查看详细讲解。
示例代码中的 create_unique 返回一个名为 ret 的局部变量,并且没有其他引用绑定到 ret 上,如果这样调用create_unique:std::unique_ptr<int> piu4 = create_unique(42); 在编译器支持 NRVO 的情况下,ret 变量不会被实际创建,而是直接在外部 piu4 的内存位置直接构造,达到消除拷贝的目的。
若编译器未支持或者代码情况不满足 NRVO 条件,移动构造则作为第二候选用来避免拷贝,拷贝构造的优先级最低,因为拷贝一个对象可能付出高昂的代价。
由于 std::unique_ptr 删除了拷贝构造和拷贝赋值函数,我们无法复制一个现有的实例;但是定义了移动构造和移动赋值函数,我们可以在函数中返回一个局部构造的实例,用以构造或者赋值给另一个 std::unique_ptr。强行转移 std::unique_ptr 的资源所有权是可以的,但是为了宣示独占性,手动转移的语法都不那么自然。
而当资源能够共享时,除了定义移动构造函数和移动赋值函数用以接管临时对象资源外,拷贝构造和拷贝赋值函数的定义显得更为重要。
