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