在C++开发者社区中,一个看似基础的语义问题近日引发了广泛讨论:函数调用时,函数体内部的表达式是否一定“先于”(sequenced before)函数调用之后的表达式执行?这个问题最早由一位匿名用户在C++标准邮件列表中提出,标题直指标准文本中是否存在明确的措辞来规定这种顺序关系。
问题的起源
提出者指出,C++标准中明确规定了函数参数的求值顺序、函数调用本身的“序列点”特性,但并未找到一句直接表述为“函数体内的表达式先于函数调用后的表达式求值”的句子。例如,在代码f(a, b) + c中,直观理解是函数f执行完毕后再计算c,但标准中相关的条款散布于多个段落,缺乏一个清晰的总括性陈述。
这一疑问并非钻牛角尖。实践中,如果程序员依赖这种顺序(例如在函数体内修改全局变量,然后在调用后使用该变量),而标准表述不明确,可能导致不同编译器产生未定义行为或意外结果。事实上,早期的C++98标准依赖“序列点”概念,而C++11引入了更精细的“先于”(sequenced before)关系,但函数调用相关规则的文本组织方式一直较为分散。
标准中的布局
记者查阅了C++17标准草案(N4659)与C++20标准(N4861)。在[expr.call]分节中,标准明确说:“调用函数(包括函数体)相对于调用者的其他表达式是按序列排序的。”(The call to a function (including the function body) is sequenced before the expression that is sequenced after the function call.)但遗憾的是,这句话并不以“The wording that says...”的形式单独出现,而是隐含在整体求值规则中。
更具体的规则出现在[intro.execution]关于“sequenced before”的定义中:若表达式A先于表达式B,则A的所有副作用在B之前产生。而函数调用本身作为一个完整的求值,其包含的整个函数体被视为该求值的一部分。因此,函数体中的所有表达式自然先于调用后的表达式。然而,标准在[expr.call] §7.6.1.2中确实明确指出:“在被调用函数执行之前,参数表达式所有值计算和副作用均已应用。”(All value computations and side effects associated with the argument expressions are sequenced before the body of the function is executed.)但并未反向说明函数体与后续表达式的关系。
社区的解读与分歧
多位C++委员会成员在邮件列表中回应说,函数体的顺序关系实际上是通过“函数调用是完整表达式的一部分”以及“sequenced before的传递性”推导出来的。例如,若 x = f() + g(),其中f和g的调用顺序未指定,但一旦某个调用开始,其整个函数体必然先于该调用之后的求值(如加法运算)。然而,语言设计专家Stephan T. Lavavej指出,标准中没有“函数体的所有部分都先于调用之后的表达式”这样的简单陈述,部分原因是函数体内可能包含未求值的操作数(如 sizeof 表达式),这些并不参与执行顺序。
更关键的是,当函数调用作为另一个函数调用的参数时(嵌套调用),顺序规则更为复杂。例如 f(g()) 中,g的函数体是否先于f的参数求值?标准明确说g的执行先于f开始(因为g的结果作为参数),但g的函数体内部是否一定先于f函数体内部的第一个表达式?答案是肯定的,这种传递性由“完整表达式”的边界保证。
对开发者的启示
这场讨论提醒我们,C++标准虽然庞大严密,但学习曲线陡峭。对于普通开发者,依赖函数调用与后续表达式的明确顺序通常是安全的(例如在析构函数中清理资源,然后使用该资源的后续代码),但要警惕未定义行为的边界。例如,int x = f() + (x = 1); 这种跨越函数调用的赋值顺序就是未定义的,因为左侧f()的副作用与右侧赋值之间没有明确的顺序关系。
目前,C++标准委员会考虑在未来的草案中添加一条直接的注释,以消除模糊性。最终提案可能类似:“函数体(包括其中的所有求值)与紧随函数调用之后的表达式具有先于关系。” 在此之前,开发者应继续依赖已有的标准推导,并优先使用代码规范(如避免在函数调用中产生跨调用的副作用依赖)来规避风险。
这一看似琐碎的措辞问题,实则折射出C++标准演进中对精确性与可读性的永恒平衡。对于任何深入使用C++的团队,理解这些底层规则,远比背诵语法条目更有价值。