Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Who copies the return value of a function?

Is it the caller or the callee copying or moving the return value of a function? For example, if I want to implement the pop() function of a queue, like this

template <typename T> 
class queue
{
    std::deque<T> d;
public:
    // ... //
    T pop()
    {
        // Creates a variable whose destructor removes the first
        // element of the queue if no exception is thrown. 
        auto guard = ScopeSuccessGuard( [=]{ d.pop_front(); } );
        return d.front();
    }
}

is the destructor of my scope guard called after copying the front element?

EDIT: Follow-up question: Would the line

auto item = q.pop();

be strongly exception-safe now?

like image 415
Ralph Tandetzky Avatar asked Jul 02 '13 08:07

Ralph Tandetzky


People also ask

Which function returns values from a function?

To return a value from a function, you must include a return statement, followed by the value to be returned, before the function's end statement. If you do not include a return statement or if you do not specify a value after the keyword return, the value returned by the function is unpredictable.

Where is the return value of a function stored?

Where is it stored? Well, it isn't stored. It actually gets erased from memory. When we call a function, the function executes its code and after the return, that particular function call gets wiped out from working memory ( the browser is in charge of the cleanup task).

What is the return value of a function called?

A function that returns a value is called a value-returning function. A function is value-returning if the return type is anything other than void . A value-returning function must return a value of that type (using a return statement), otherwise undefined behavior will result.

Does return return a copy C++?

commercial-grade C++ compilers won't do that: the return statement will directly construct x itself. Not a copy of x, not a pointer to x, not a reference to x, but x itself.


1 Answers

The return value is copied out before the local variables go out of scope. The copy/move might be to a temporary location (stack or register(s)) or directly to the caller's own buffer or preferred registers - that's an optimisation/inlining issue.

Where a temporary location's involved the compiler must arrange some division of work between the caller and callee, and there are a number of OS- and binary object/executable-format-specific conventions for return values (and function parameters of course), such that libraries/objects compiled with one compiler can typically still be used with another.

Would the line...

auto item = q.pop();

...be strongly exception safe?

Assuming pop_front() can't throw, the interesting case is where a temporary location is returned, from which the value is again copied into the caller buffer after the function's returned. It would seem to me that you haven't protected adequately against that. Elision (the callee directly constructing the return value in the caller's result buffer/register(s)) is permitted but not required.

To explore this, I've written the following code:

#include <iostream>

struct X
{
    X() { std::cout << "X::X(this " << (void*)this << ")\n"; }
    X(const X& rhs) { std::cout << "X::X(const X&, " << (void*)&rhs
                                << ", this " << (void*)this << ")\n"; }
    ~X() { std::cout << "X::~X(this " << (void*)this << ")\n"; }

    X& operator=(const X& rhs)
    { std::cout << "X::operator=(const X& " << (void*)&rhs
                << ", this " << (void*)this << ")\n"; return *this; }
};

struct Y
{
    Y() { std::cout << "Y::Y(this " << (void*)this << ")\n"; }
    ~Y() { std::cout << "Y::~Y(this " << (void*)this << ")\n"; }
};

X f()
{
   Y y;
   std::cout << "f() creating an X...\n";
   X x;
   std::cout << "f() return x...\n";
   return x;
};

int main()
{
    std::cout << "creating X in main...\n";
    X x;
    std::cout << "x = f(); main...\n";
    x = f();
}

Compiling with g++ -fno-elide-constructors, my output (with extra comments) was:

creating X in main...
X::X(this 0x22cd50)
x = f(); main...
Y::Y(this 0x22cc90)
f() creating an X...
X::X(this 0x22cc80)
f() return x...
X::X(const X&, 0x22cc80, this 0x22cd40)   // copy-construct temporary
X::~X(this 0x22cc80)   // f-local x leaves scope
Y::~Y(this 0x22cc90)
X::operator=(const X& 0x22cd40, this 0x22cd50)  // from temporary to main's x
X::~X(this 0x22cd40)
X::~X(this 0x22cd50)

Clearly, the assignment happened after f() left scope: any exception therefrom would be after your scope guard (here represented by Y) had been destroyed.

The same kind of thing happens if main contains X x = f(); or X x(f());, except it's the copy constructor that's invoked after destruction of the f()-local variables.

(I appreciate that one compiler's behaviour is sometimes a poor basis for reasoning about whether something is required by the Standard to work, but it's considerably more reliable the other way around: when it doesn't work either that compiler's broken - which is relatively rare - or the Standard doesn't require it. Here, the compiler behaviour's just used to add anecdotal weight to my impression of the Standard's requirements.)

Fiddly details for the curious: not that it's typically useful to have code that can only be called in one way, but something that might be safe is const X& x = f();, as the const reference extends the lifetime of the temporary, but I can't convince myself that the Standard requires to have the temporary whose lifetime's extended be the temporary the function copied into sans any additional copy; for what little it's worth - it "worked" in my program and interestingly the temporary occupies the same stack location used if eliding a return value, which suggests f() code is effectively compiled with an ability to elide and the -f-no-elide-constructors option is not so much disabling an optimisation as going out of its way to add a pessimisation: leaving additional stack space for a temporary before calling the function then adding the extra code to copy therefrom and destruct the temporary then readjust the stack pointer....

like image 184
Tony Delroy Avatar answered Sep 28 '22 05:09

Tony Delroy