C++ 函数中 "return" 的精确时刻

Exact moment of "return" in a C++-function

这似乎是一个愚蠢的问题,但是 return xxx; 在明确定义的函数中 "executed" 的确切时刻是什么?

请看下面的例子明白我的意思(here live):

#include <iostream>
#include <string>
#include <utility>

//changes the value of the underlying buffer
//when destructed
class Writer{
public:
    std::string &s;
    Writer(std::string &s_):s(s_){}
    ~Writer(){
        s+="B";
    }
};

std::string make_string_ok(){
    std::string res("A");
    Writer w(res);
    return res;
}


int main() {
    std::cout<<make_string_ok()<<std::endl;
} 

我天真地期望发生什么,而 make_string_ok 被称为:

  1. 调用 res 的构造函数(res 的值为 "A"
  2. w 的构造函数被调用
  3. return res 被执行。应返回 res 的当前值(通过复制 res 的当前值),即 "A".
  4. w的析构函数被调用,res的值变为"AB"
  5. 调用了 res 的析构函数。

所以我希望得到 "A" 结果,但在控制台上打印 "AB"

另一方面,make_string的一个稍微不同的版本:

std::string make_string_fail(){
    std::pair<std::string, int> res{"A",0};
    Writer w(res.first);
    return res.first;
}

结果如预期 - "A" (see live).

标准是否规定了上面示例中应返回的值还是未指定?

由于 Return Value Optimization (RVO)make_string_okstd::string res 的析构函数可能不会被调用。 string 对象可以在调用方构造,函数只能初始化值。

代码将等同于:

void make_string_ok(std::string& res){
    Writer w(res);
}

int main() {
    std::string res("A");
    make_string_ok(res);
}

这就是为什么值 return 应为 "AB"。

在第二个示例中,RVO 不适用,并且在调用 return 和 Writer 的析构函数时,该值将被复制到 returned 值将在复制发生后 res.first 运行。

6.6 Jump statements

On exit from a scope (however accomplished), destructors (12.4) are called for all constructed objects with automatic storage duration (3.7.2) (named objects or temporaries) that are declared in that scope, in the reverse order of their declaration. Transfer out of a loop, out of a block, or back past an initialized variable with automatic storage duration involves the destruction of variables with automatic storage duration that are in scope at the point transferred from...

...

6.6.3 The Return Statement

The copy-initialization of the returned entity is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement, which, in turn, is sequenced before the destruction of local variables (6.6) of the block enclosing the return statement.

...

12.8 Copying and moving class objects

31 When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.(123) This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one object destroyed for each one constructed.

C++ 中有一个概念叫做省略。

Elision 采用两个看似不同的对象并合并它们的身份和生命周期。

之前可能发生省略:

  1. 当你在函数中有一个非参数变量 Foo f; returned Foo 并且 return 语句是一个简单的 return f;.

  2. 当你有一个匿名对象被用来构造几乎任何其他对象时。

中,所有(几乎?)#2 的情况都被新的纯右值规则消除了;省略不再发生,因为用于创建临时对象的东西不再这样做。相反,"temporary" 的构造直接绑定到永久对象位置。

现在,考虑到编译器编译到的 ABI,省略并不总是可能的。可能的两种常见情况被称为 Return 价值优化和命名 Return 价值优化。

RVO是这样的:

Foo func() {
  return Foo(7);
}
Foo foo = func();

其中我们有一个 return 值 Foo(7),它被省略到值 returned 中,然后又被省略到外部变量 foo 中。看起来是 3 个对象([=18= 的 return 值、return 行上的值和 Foo foo)在运行时实际上是 1。

之前,copy/move构造函数必须存在于此,省略是可选的;在 中,由于新的纯右值规则,不需要存在 copy/move 构造函数,并且编译器没有选项,这里必须有 1 个值。

另一个著名案例是 return 价值优化,NRVO。这是上面的 (1) 省略案例。

Foo func() {
  Foo local;
  return local;
}
Foo foo = func();

同样,省略可以合并 Foo local 的生命周期和身份,func 中的 return 值和 func 之外的 Foo foo

甚至 ,第二次合并(在 func 的 return 值和 Foo foo 之间)是非可选的(技术上 prvalue returned from func 从来都不是一个对象,只是一个表达式,然后绑定到构造 Foo foo),但第一个仍然是可选的,并且需要一个移动或复制构造函数才能存在。

Elision 是一种规则,即使消除这些副本、破坏和构造会产生明显的副作用,它也会发生;这不是 "as-if" 优化。相反,它与天真的人可能认为的 C++ 代码的含义有细微的差别。称它为 "optimization" 有点用词不当。

事实上它是可选的,而且微妙的事情会破坏它,这是一个问题。

Foo func(bool b) {
  Foo long_lived;
  long_lived.futz();
  if (b)
  {
    Foo short_lived;
    return short_lived;
  }
  return long_lived;
}

在上述情况下,虽然编译器省略 Foo long_livedFoo short_lived 是合法的,但实现问题使其基本上不可能,因为两个对象的生命周期不能与return func 的值;一起省略 short_livedlong_lived 是不合法的,它们的生命周期重叠。

您仍然可以在 as-if 下进行操作,但前提是您能够检查并理解析构函数、构造函数和 .futz().

的所有副作用

它是 RVO(+ return 复制为临时图像,使图片模糊不清),允许更改可见行为的优化之一:

10.9.5 Copy/move elision (重点是我的):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects**. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object.

This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function parameter or a variable introduced by the exception-declaration of a handler) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function call's return object
  • [...]

根据它是否应用,你的整个前提都错了。在 1. 调用 res 的 c'tor,但对象可能位于 make_string_ok 内部或外部。

案例一

第 2 条和第 3 条可能根本不会发生,但这是一个附带问题。目标受到 Writers dtor 影响的副作用,在 make_string_ok 之外。这恰好是在评估 operator<<(ostream, std::string) 的上下文中使用 make_string_ok 创建的临时文件。编译器创建了一个临时值,然后执行该函数。这很重要,因为 temporary 存在于它之外,因此 Writer 的目标不是 make_string_ok 的本地目标,而是 operator<<.

的目标

案例二

同时,您的第二个示例不符合标准(也不符合为简洁起见省略的标准),因为类型不同。所以作者死了。如果它是 pair 的一部分,它甚至会死亡。所以在这里,res.first的副本被returned作为一个临时对象,然后Writer的dtor影响了原来的res.first,它即将死亡。

很明显,复制是在调用析构函数之前创建的,因为复制的对象 return 也被销毁了,否则您将无法复制它。

毕竟它归结为 RVO,因为 Writer 的 d'tor 要么作用于外部对象,要么作用于本地对象,这取决于是否应用了优化。

标准是否规定了上面示例中应 return 的值还是未指定?

不,优化是可选的,尽管它可以改变可观察到的行为。是否应用它由编译器自行决定。 它不受“一般假设”规则的约束,该规则表示允许编译器进行任何不改变可观察行为的转换。

在 c++17 中,它的案例成为强制性的,但不是你的。强制性的是 return 值是一个未命名的临时值。