函数参数的延迟销毁

15
根据n4640中的5.2.2/4 "函数调用"(在n4659中的8.2.2/4),函数参数在调用者的上下文中创建和销毁。实现允许将函数参数的销毁延迟到封闭完整表达式的末尾(作为实现定义的特性)。注意,选择不是未指定的,而是实现定义的。
(3.3.3的意思似乎是暗示函数参数具有块作用域,然后3.7.3表示局部变量存储持续到创建它们的块退出。但假设我在措辞上遗漏/误解了一些内容。)显然现在函数参数将拥有它们自己的作用域
据我所知,ABI要求GCC和Clang将函数参数的销毁延迟到完整表达式的末尾,即这是这些编译器的实现定义行为。我猜想,在这种实现中,只要这些引用/指针仅在调用表达式内使用,将函数参数的引用/指针返回应该是可以的。
但是,在GCC中以下示例会导致段错误,在Clang中正常工作。
#include <iostream>
#include <string>

std::string &foo(std::string s)
{
  return s;
}

int main()
{
   std::cout << foo("Hello World!") << std::endl;
}

两个编译器都警告返回局部变量的引用,这在这里是完全适当的。检查生成的代码可以快速发现,两个编译器确实延迟参数的销毁到表达式的末尾。然而,GCC仍然故意从foo返回一个“空引用”,导致崩溃。与此同时,Clang表现“如预期”地返回其参数s的引用,该引用存活足够长的时间以产生预期的输出。

(在这种情况下,通过简单地执行以下操作,可以轻松欺骗GCC

std::string &foo(std::string s)
{
  std::string *p = &s;
  return *p;
}

(该补丁解决了在GCC下的分段错误。)

在这种情况下,GCC的行为是否合理,假设它保证参数的“延迟”销毁?我是否遗漏了标准中的其他某些部分,即使实现扩展了它们的生命周期,返回函数参数的引用始终是未定义的?


我不是语言律师,尽管在电视和工作中扮演这样一个角色。"Hello world"是一个参数;s是一个参数。参数s的块作用域在foo最外层块作用域返回时结束。 - Eljay
@Eljay:您的术语使用正确。但5.2.2章节特别讨论的是“参数”。对应的文本变化是由DR#1880(http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1880)引起的,该文档也特别讨论了“参数”。如果是“参数”,问题甚至不会出现。 - AnT stands with Russia
我猜你指的是N4659中的8.2.2/4(expr.call/4),而不是“5.2.2/4”。 - M.M
请问这个问题适用于哪个标准版本?我假设是C++17,但在未来这将不明显。 - eerorika
我猜测gcc返回一个空引用,以作为一种措施来覆盖误用,当返回本地/参数时。 - kmdreko
核心问题1880仍然未解决。这个变化是由P0135R1作为保证复制省略更改的一部分所做的。请注意,在1880的指导下(未指定),即使编译器在完整表达式结束时总是销毁参数,该代码也存在[介绍.抽象]/5的UB。 - T.C.
3个回答

7
据我所知,ABI要求GCC和Clang将函数参数的销毁延迟到完整表达式结束时才进行。这个问题在很大程度上依赖于这个假设。Itanium C++ ABI草案3.1.1 Value Parameters表示:“如果该类型具有非平凡析构函数,则调用方在控制返回给它后(包括当调用方抛出异常时),在封闭的完整表达式结束时调用该析构函数。” ABI没有定义“生命周期”,因此让我们检查C ++标准草案N4659 [basic.life]:“1.2 ...类型为T的对象o的生命周期在以下情况下结束:1.3如果T是具有非平凡析构函数(15.4)的类类型,则析构函数调用开始,或者...1.4释放对象占用的存储器,或被不嵌套在o内的对象重用([intro.object])。
C++标准规定,在析构函数被调用时,生命周期将终止。因此,ABI确实要求函数参数的生命周期应该延伸到函数调用的完整表达式。假设遵循这个实现定义的要求,在这个示例程序中,我没有看到任何未定义行为,因此它应该在任何保证遵循Itanium C++ ABI的实现中具有预期行为。GCC似乎违反了这一规定。GCC文档指出,从版本3开始,GNU C++编译器使用了业界标准的C++ ABI,即Itanium C++ ABI。因此,这种表现可以被认为是一个错误。但另一方面,我们不清楚[expr.call]的这种改变是否是故意的。这种后果可能被认为是一个缺陷。

......这意味着块范围变量的存储持续到它们被创建的块退出。

确实如此。但是你引用的[expr.call]/4说:“函数参数在调用者的上下文中被创建和销毁”。因此,存储持续到函数调用块的末尾。似乎与存储持续时间没有冲突。


请注意,C++标准链接指向的是一个定期从当前草案生成的网站,因此可能与我引用的N4659不同。

在 C++ 标准中,“上下文”一词通常用于访问控制或模板参数替换。随后的注释似乎表明上下文仅约束访问,而不约束生存期或存储持续时间。 - Oliv

-2
从5.2.2/4函数调用[expr.call]来看,我认为GCC是正确的:

参数的生命周期在定义它的函数返回时结束。每个参数的初始化和销毁都发生在调用函数的上下文中。


你引用的是哪个标准的版本?另外,请提及章节名称或括号内的简写,因为不同版本的编号可能会有所不同。 - eerorika
N3690,http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf - AdvSphere
N3690是C++14之前的草案。现在我们已经过了C++17。 - M.M
在N4659/8.2.2/4中,文本几乎相同。“参数的生命周期是否在定义它的函数返回时结束,或在封闭的完整表达式的结尾处结束是实现定义的。每个参数的初始化和销毁都发生在调用函数的上下文中。” - Eljay
1
据我所知,N4659与C++17相同,而N4700是C++17之后的草案。因此,我建议使用N4659。@user167921 - M.M
显示剩余2条评论

-2

抱歉,我之前的回答是基于 C++14 标准的,现在看来根据 C++17 的规定,GCC 和 Clang 都是正确的:

引用自:N4659 8.2.2/4 函数调用 [expr.call]

对于参数的生命周期结束时间,实现是有定义的。它可能在包含它的完整表达式结束时结束,也可能在定义它的函数返回时结束。每个参数的初始化和销毁都发生在调用函数的上下文中。


@user167921:“在假设GCC保证参数的“延迟”销毁的情况下,这种行为是否合理?”这只是一个假设。 - AdvSphere

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接