谈谈RAII惯用法-具名对象表达式RAII
转发公众号文章:谈谈RAII惯用法-具名对象表达式RAII
从本篇开始介绍一些特殊场景的RAII应用方式。
所谓表达式RAII,是指RAII对象是在一个表达式语句中创建的,不同于上篇文章中介绍的通过作用域{}来管理RAII对象的生存期,它们有自己独特的生存期管理,在某些场景下为程序代码实现带来了方便性。
先看一个例子:
if (条件) {
raii_ptr raii(42);
...其它用到raii的代码
}
在if语句块中用到了raii对象,它在语句块结束的地方,即“}”位置,会自动调用它的析构函数,显然这是RAII机制的最常见的用法。
但是,如果else语句块中也用到了raii对象,或者在if语句的条件表达式中用到了raii对象,raii对象只好在if语句的前面进行创建,即类似下面的代码片段:
raii_ptr raii(42);
if (条件) {
...用到raii的代码
} else {
...用到raii的代码
}
和
raii_ptr raii(42);
if (用到raii的条件表达式) {
...用到raii的代码
}
尽管raii对象在if语句运行结束后就不再使用了,但是它却要等到离开生存期结束时,比如遇到“}”或者函数返回时才被销毁,此时可能距离if语句已经很远了,显然raii对象没有及时销毁,所管理的资源也就不会及时释放。
因此,程序员为了在raii对象不再使用时,能够尽快地销毁它,就使用{}对raii对象所在的作用域进行了限制:
{
raii_ptr raii(42);
if (条件) {
...用到raii的代码
} else {
...用到raii的代码
}
} // A
和
{
raii_ptr raii(42);
if (用到raii的条件表达式) {
...用到raii的代码
}
} // B
这样,就不用等到函数返回,而是在A和B处就对raii对象进行了销毁操作。当然,不足之处是需要程序员手动添加"{}"代码来把raii对象和if语句限定在指定的作用域中,在编程时稍有不便。
C++17标准中提供了带初始化的if和switch语句的新特性,或者说是编译器语法糖,可以利用该特性使用更为简洁的方式来实现上述RAII对象生存期的管理。
1、if语句作用域RAII
先看标准“9.4.1 The if statement”章节中的描述:
An if statement of the form if constexpropt ( init-statement condition
) statement is equivalent to {
init-statement
if constexpropt ( condition ) statement }
和
And an if statement of the form if constexpropt ( init-statement
condition ) statement else statement is equivalent to {
init-statement
if constexpropt ( condition ) statement else statement }
由此可见,可以在if语句的条件表达式中声明并初始化变量,等同于它是在if语句之前定义的,并且使用“{}”限定了作用域,因此这个变量的生存期:
1、在整个if语句中有效,包括条件表达式部分、then部分和else部分。
2、仅在整个if语句中有效,因为等同于在if语句的外围添加了“{}”。
因此根据该特性,如果把初始化变量换成初始化一个RAII对象,则可以把上述场景改成下面的形式:
void demo(int flag) {
// ... 其它代码
if (raii_ptr raii(42); flag > 0) {
// ...使用raii对象的代码
} else {
// ...使用raii对象的代码
} // C
// ...其它不使用raii对象的代码
}
直接在if语句的表达式中创建raii对象,这个对象在if-else语句块中都是可见的,并且在C处离开}时,编译器会生成调用析构函数的代码。即RAII的作用域会覆盖所有的选择分支,相当于编译器生成了下面的代码:
void demo(int flag) {
// ... 其它代码
raii_ptr raii(42);
if (flag > 0) {
// ...使用raii对象的代码
} else {
// ...使用raii对象的代码
}
raii.~raii_ptr(); // 编译器生成调用析构函数代码
// ...其它不使用raii对象的代码
}
显然,这种方式更为简洁和方便:编写代码时,不再手动编程使用“{}”来限定raii对象的生存期作用域;阅读代码时,也无需去查找“{”和“}”对应的匹配位置。
2、switch语句作用域RAII
下面是标准在“9.4.2 The switch statement”章节中的相关描述:
A switch statement of the form switch ( init-statement condition )
statement is equivalent to {
init-statement
switch ( condition ) statement }
可见,switch语句也有类似的用法,例如下面的代码:
void demo(int x) {
// ... 其它代码
switch (raii_ptr raii(42); x) {
case 1:
// ... 使用raii对象的代码
break;
case -1:
// ... 使用raii对象的代码
break;
default:
// ... 使用raii对象的代码
break;
}
// ... 其它不使用raii对象的代码
}
raii对象的作用域会覆盖所有的case选择分支,相当于编译器生成了下面的代码:
void demo(int x) {
// ... 其它代码
raii_ptr raii(42);
switch (x) {
case 1:
// ... 使用raii对象的代码
break;
case -1:
// ... 使用raii对象的代码
break;
default:
// ... 使用raii对象的代码
break;
}
raii.~raii_ptr(); // 编译器生成调用析构函数的代码
// ... 其它不使用raii对象的代码
}
3、循环语句没有这样的特性
我们知道,在下面C++中的for循环语句块中:
for (int i=0, j=0; i<10; i++) {
// ...可以访问变量i,j
}
变量i和j都是在for循环表达式的初始化部分中定义的,它们也可以在for循环语句块中被访问,而且作用域仅限于for循环的内部,貌似也可以像if和switch那样,在for循环语句块中使用RAII对象。但是,如果有多个变量需要在初始化部分中初始化,它们只能在是相同类型的变量时才可以这样做,RAII对象和循环变量的类型往往是不相同的。因此,如果在for的初始化部分中直接定义RAII对象,会因为与循环变量类型不同而无法编译。
当然,可以使用别的方式来变通一下,比如使用std::pair来封装RAII对象和循环变量:
for (std::pair<raii_ptr, int> p(42, 0); p.second < 10; p.second++) {
...其它使用p.first(即raii_ptr对象)的代码
}
显然,这样用起来也并不方便,程序晦涩聱牙,为评审和维护代码带来了心智负担,并不建议这样使用,在实际应用时,应该使用“{}”来限定RAII对象的作用域:
{
raii_ptr raii(42);
for (int i=0; i<10; i++) {
// ... 其它使用raii对象的代码
}
}
同样while循环语句,也没有这样的语法特性。如果RAII对象的作用域仅限于while语句块,不妨使用“{}”来限定RAII的作用域:
{
raii_ptr raii(42);
while (i++ < 10) {
// ... 其它使用raii对象的代码
}
}
4、RAII对象必须是具名对象
最后,需要注意的是,无论是带初始化的if还是switch,初始化的RAII对象都不能是匿名的,它们都必须是具名的。假设在if语句中使用mutex来保护整个if-else语句块,如果这样来创建lock_guard类型的RAII对象:
mutex mtx; // 互斥对象
void demo(int flag) {
// ... 其它代码
if (lock_guard(mtx); flag > 0) {
// ...mtx保护的代码
} else {
// ...mtx保护的代码
} //
// ...其它不使用raii对象的代码
}
编译器编译后,它相当于是这样的:
void demo(int flag) {
// ... 其它代码
lock_guard tmp(mtx);
tmp.~lock_guard(); // 创建后紧接着就销毁了
if (flag > 0) {
// ...其它代码
} else {
// ...其它代码
} //
// ...其它代码
}
可见,创建的RAII对象,生存期并不在if-else语句块的作用域范围,在程序还没有进入if-else语句块时,它就被销毁了,因此,这里的mutex对象并没有能够保护if-else语句块的线程安全。
不过,这种匿名RAII对象,有着特殊的生存期语义,在一些场合能非常方便的解决一些常规方法不能解决的问题,下一篇文章来介绍。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)