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

ead :

It seems like a silly question, but is the exact moment at which return xxx; is "executed" in a function unambiguously defined?

Please see the following example to see what I mean (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;
} 

What I naively expect to happen, while make_string_ok is called:

  1. Constructor for res is called (value of res is "A")
  2. Constructor for w is called
  3. return res is executed. The current value of res should be returned (by copying the current value of res), i.e. "A".
  4. Destructor for w is called, the value of res becomes "AB".
  5. Destructor for res is called.

So I would expect "A"as result, but get "AB" printed on the console.

On the other hand, for a slightly different version of make_string:

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

the result is as expected - "A" (see live).

Does the standard prescribes which value should be returned in the examples above or is it unspecified?

luk32 :

It's RVO (+ returning copy as temporary which fogs the picture), one of the optimization that are allowed to change visible behaviour:

10.9.5 Copy/move elision (emphases are mine):

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
  • [...]

Based on whether it's applied your whole premise gets wrong. At 1. the c'tor for res is called, but the object might live inside of make_string_ok or outside.

Case 1.

Bullets 2. and 3. might not happen at all, but this is a side point. Target got side effects of Writers dtor affected, was outside of make_string_ok. This happened to be a temporary created by using the make_string_ok in the context of evaluation operator<<(ostream, std::string). The compiler created a temporary value, and then executed the function. This is important because temporary lives outside of it, so the target for Writer is not local to make_string_ok but to operator<<.

Case 2.

Meanwhile, your second example does not fit the criterion (nor the ones omitted for brevity) because the types are different. So the writer dies. It would even die, if it were a part of the pair. So here, a copy of res.first is returned as a temporary object, and then dtor of Writer affects the original res.first, which is about to die itself.

It seems pretty obvious that the copy is made before calling destructors, because the object returned by copy is also destroyed, so you'd not be able to copy it otherwise.

After all it boils down to RVO, because the d'tor of Writer either works on the outside object or on the local one, according to whether the optimization is applied or not.

Does the standard prescribes which value should be returned in the examples above or is it unspecified?

No, the optimization is optional, though it can change the observable behaviour. It's at the compiler's discretion to apply it or not. It's an exempt from the "general as-if" rule which says compiler is allowed to make any transformation which does not change observable behaviour.

A case for it became mandatory in c++17, but not yours. The mandatory one is where the return value is an unnamed temporary.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related