函数参数的销毁顺序是什么?

36
如果某个带有类型为T_1, ..., T_n的参数p_1, ..., p_n的函数f被调用时,使用参数a_1, ..., a_n,并且当其主体抛出异常、结束或返回时,参数的销毁顺序是什么,为什么?如果可能,请提供对标准的引用。

编辑: 实际上我想问的是关于函数“参数”的问题,但是由于T.C.和Columbo已经澄清了我的困惑,所以我保留这个问题关于参数,同时提出一个新的单独问题来区分两者。请参见此问题的评论。


1
我不知道顺序,但我猜第二个问题的答案是“因为标准如此”。 - zmbq
2
我认为没有预定义的顺序(与调用具有多个参数的函数时相同),但我很乐意看到引用标准的答案。好问题,+1。 - vsoftco
1
http://wg21.link/cwg1880。这似乎未明确说明。 - T.C.
1
@Columbo 不完全是这样。我经常看到人们混用它们。因为参数的生命周期不一定与函数退出有关 - 例如 void foo(std::string); std::string s; foo(s); - T.C.
5
@Columbo,我认为我从未遇到过那么苛求区别的人。我认识的大多数人都将参数和实参视作同义词。在main函数中,使用的是argvargc,而不是paramvparamc - Rob K
显示剩余15条评论
3个回答

22

我没有在标准中找到答案,但我能够在三个最流行的C++编译器上进行测试。R Sahu的回答基本上解释了这是实现定义。

§5.2.2 / 8:后缀表达式和参数的计算在彼此之间没有排序关系。所有参数评估的副作用都在进入函数之前排序。

Visual Studio C++ 编译器(Windows)和 gcc(Debian)
参数按照它们的声明顺序相反的顺序构造,并以相反的顺序销毁(因此按照声明顺序销毁):

2
1
-1
-2

Clang(FreeBSD)
参数按照它们的声明顺序构造并以相反的顺序销毁:

1
2
-2
-1

所有编译器都被指示将源代码视为C++11,并使用以下代码片段来演示情况:

struct A
{
    A(int) { std::cout << "1" << std::endl; }
    ~A() { std::cout << "-1" << std::endl; }
};

struct B
{
    B(double) { std::cout << "2" << std::endl; }
    ~B() { std::cout << "-2" << std::endl; }
};

void f(A, B) { }

int main()
{
    f(4, 5.);
}

14

在§5.2.2[4]中,N3337非常明确地阐述了发生的情况(在线草案):

在参数初始化期间,实现可以通过将相关实参的转换和/或临时对象的构造与参数的初始化组合起来,以避免构造额外的临时对象 (见12.2)。参数的生命周期在定义该参数的函数返回时结束。

例如,在以下情况下:

f(g(h()));

调用h()的返回值是一个临时对象,将在完整表达式结束时被销毁。然而编译器允许避免使用这个临时对象,并直接使用它的值初始化g()的参数。在这种情况下,返回值将在g()返回之前被销毁(即在调用f()之前)。

如果我正确理解标准中所述的内容,那么除非进行复制(参数),否则不允许使从h()返回的值在完整表达式结束之前存活。

两种情况如下:

  1. h的返回值用于直接初始化g的参数。该对象在g返回之前被销毁,并在调用f之前。
  2. h的返回值是一个临时对象。复制一份以初始化g参数,并在g返回时销毁该副本。原始临时对象在完整表达式结束时被销毁。

我不知道实现是否遵循这些规则。


T.C.在调用者的上下文中是临时对象,在被调用者的上下文中是命名对象。 - 6502
3
“他们在呼叫者的背景下是临时的” {{需要引证}} - T.C.
@6502 对于我混淆参数和参数所引起的困惑,我感到很抱歉。由于我询问了关于参数的问题,我认为您目前划掉的答案部分最好地回答了这个问题。请查看我对问题的编辑以及问题下面的评论。我认为最好将参数视为在调用函数之前分配给参数的东西。因此,在某些函数调用表达式中讨论参数的销毁只有在编译器决定使用临时变量时才有意义。否则,这是一个参数销毁的情况,与CWG#1880有关。 - jotik
2
@jotik:不幸的是,C++中关于这个问题的模糊性仍然存在。问题在于临时变量应该在完整表达式的末尾被销毁,但参数可能在调用结束时被销毁(!)。理论上,这意味着一个C++编译器,在被调用者销毁参数的情况下,必须始终从临时参数复制一个副本,以便被调用者可以销毁副本,但临时参数等待函数结束。我不认为任何编译器编写者会这样做来遵守那些荒谬的规则。我想(希望)。 - 6502
@Barry:不好意思,这些参数可能是临时的(在整个表达式结束时以创建的相反顺序销毁),但也可能直接构建到参数中,在调用返回时销毁(可能会在整个表达式结束之前发生)。你为什么觉得这不是回答问题的呢? - 6502
显示剩余10条评论

12
函数参数的求值顺序在标准中没有指定。根据C++11标准 (在线草案):

5.2.2 函数调用

8 [ 注: 后缀表达式和实参表达式的求值之间是未排序的。所有实参表达式的副作用都在进入函数之前被排序 (见1.9). —end note ]

因此,完全由实现来决定评估函数参数的顺序。这又意味着参数的构造顺序也取决于实现。
可行的实现将以它们构造的相反顺序销毁对象。

3
“平台相关”指的是“实现定义”吗? - jotik
@jotik,是的。"平台相关"是口语术语 :) - R Sahu
@RSahu 举个例子,GCC是一个可以在许多平台上运行的编译器,它在Linux和Windows上可能具有相同的实现定义行为,Clang也是如此。规范使用术语实现定义来表示由实现者决定此处发生的事情;这与平台本身几乎没有关系。 - cat
1
@cat,我们在这里纠结于细节。 - R Sahu
3
我认为正确的术语实际上是“未指定”。ISO确实将“实现定义”作为一类,但这是一个更强的要求:它实际上意味着实现必须公开定义他们所做的选择。未指定意味着该选择可能因版本而异,甚至可能取决于编译器设置。 - MSalters
@MSalters,同意。了解未指定行为和实现定义行为之间微妙但重要的区别是很好的。 - R Sahu

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