Who copies the return value of a function?
Asked Answered
C

2

8

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?

Container answered 2/7, 2013 at 8:53 Comment(1)
Yes, variables with automatic storage duration (like your guardhere) are destroyed after the return instructions (and its constructions/copies). You can check the standard part 6.6 & 6.7 on jump instructions and declarations. Maybe easier you can try it with dummy objects printing something at destruction ^^Nigrify
M
9

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....

Mutant answered 2/7, 2013 at 9:0 Comment(0)
D
6

The copy of the return value is done by the callee, and must be made before destructors are called, since otherwise you couldn't return the value/contents of a locally constructed variable.

Here's the relevant section in the standard: Section 12.4, point 11 (destructors)

Destructors are invoked implicitly

  • for constructed objects with automatic storage duration (3.7.3) when the block in which an object is created exits (6.7)

I was trying to find a place where it said that the "return happens before the destruction", but it doesn't state that as clearly as I would like [unless I'm missing something].

Diagenesis answered 2/7, 2013 at 9:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.