C++避坑---函数参数求值顺序和使用独立语句将newed对象存储于智能指针中

2023-05-17 21:58:16 浏览数 (2)

函数参数求值顺序

首先我们看一个例子:

代码语言:javascript复制
#include <iostream>
using namespace std;
char a() {
  cout << "a" << endl;
  return 'a';
}
char b() {
  cout << "b" << endl;
  return 'b';
}
char c() {
  cout << "c" << endl;
  return 'c';
}

void z(char, char, char) {}

int main() {
  z(a(), b(), c());
  return 0;
}

输出结果(不同编译器输出结果可能不同):

代码语言:javascript复制
c
b
a

你可能会很诧异:在z(a(), b(), c());中,不应该是按照参数顺序来调用函数a() b()c()吗?实际上C 对于这种函数参数求值顺序通常情况下是未指明的,也就是说:大部分情况下,编译器能在任何操作数和其他子表达式中以任何顺序求值,并且可以在再次求值同一表达式时选择另一顺序。

为什么C 不把顺序规定清楚呢?实际上这是C 设计者故意而为之的,因为C 在平衡功能的同时,还要追求高的执行效率。允许编译器在优化中根据实际需要调整实现表达式求值的指令顺序,从而达到更高效的执行效率。

newed对象与智能指针

我们使用《 Effective C 》中的例子,假设有两个函数priorityprocessWight,其对应的原型如下:

代码语言:javascript复制
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

如果采用下面的方法传参并调用processWidget函数,在C 17以前,则有可能造成资源泄漏。

代码语言:javascript复制
processWidget(std::shared_ptr<Widget>(new Widget()), priority());

结合上一章节的探讨,很容易发现问题的关键。首先我们分析一下在调用processWidget函数之前,编译器需要做哪些事情:

  1. 调用new Widget()表达式(动态创建Widget对象)。
  2. 调用shared_ptr<Widget>的构造函数(使用Widget对象的指针作为构造参数)。
  3. 调用priority函数。

由于C 中针对函数参数求值顺序未进行明确定义,因此编译器可以根据实际情况来调整上述事情的顺序。当编译器采用1、3、2的顺序:

  • 调用new Widget()表达式。
  • 调用priority函数。
  • 调用shared_ptr<Widget>类的构造函数。

进行编译的时候,如果在3,也就是调用priority函数过程中发生异常,无法执行到2,那么new Widget()表达式动态创建的对象就不会被shared_ptr<Widget>跟踪管理,就有可能造成内存泄漏。

解决这样的问题办法也很简单,就是使用分离语句,将std::shared_ptr<Widget>(new Widget())拎出来,在单独的语句中执行new Widget()表达式和shared_ptr<Widget>构造函数的调用,完成“资源被创建”和“资源被管理对象接管”的无缝操作后,将智能指针传给processWidget函数。保证“资源被创建”和“资源被管理对象接管”之间不会发生任何干扰。代码实现如下:

代码语言:javascript复制
std::shared_ptr<Widget> pw(new Widget());
processWidget(pw, priority());

这样的实现,就是利用了编译器对于跨越语句的各项操作没有重新排列的自由,只有在语句内才拥有某种自由度的特性。最终规避了内存泄露的风险。

C 17带来的好消息

在上一章节中,我们提到processWidget(std::shared_ptr<Widget>(new Widget()), priority());语句可能带来内存泄漏的风险,建议我们使用独立语句避免该风险。然而在C 17中,新规则禁止交错,其原文内容如下:

When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function. For each function invocation F, for every evaluation A that occurs within F and every evaluation B that does not occur within F but is evaluated on the same thread and as part of the same signal handler (if any), either A is sequenced before B or B is sequenced before A.[49] [49] In other words, function executions do not interleave with each other. From N4868, October 2020, Draft

根据新的规则,对于函数的参数的计算不会相互交错。也就是说,从C 17起,对于

代码语言:javascript复制
processWidget(std::shared_ptr<Widget>(new Widget()), priority());

不允许编译器采用1、3、2的那种顺序,上述语句只存在(1、2)、3和3、(1、2)两种有效顺序,虽然这两种顺序仍是编译器可以根据实际情况自行选择的,但这两种方案对我们来说都是安全的,因为它们能够保证“资源被创建”和“资源被管理对象接管”之间(也就是1、2之间)不会发生任何干扰。因此就不会存在我们担心的内存泄漏的问题。

总 结

虽然C 17已经能够规避到我们上面讨论过的风险,但是考虑到我们代码的普适性,仍建议我们:使用独立语句将newed对象存储于智能指针中,来保证“资源被创建”和“资源被管理对象接管”之间不会发生任何干扰,进而确保动态获取的资源一定能够被资源管理对象接管,避免发生内存泄漏风险。

参考文献 《Effective C 第三版》 N4868, October 2020, Draft

0 人点赞