Does return-by-value mean extra copies and extra overhead?
Not necessarily.
All(?) commercial-grade compilers optimize away the extra copy, at least in cases as illustrated in the previous FAQ.
To keep the example clean, let's strip things down to the bare essentials. Suppose yourCode() calls rbv() ("rbv" stands for "return by value") which returns a Foo object by value:
class Foo { ... };
Foo rbv();
void yourCode()
{
Foo x = rbv(); © the return-value of rbv() goes into x
...
}
Now the question is, How many Foo objects will there be? Will rbv() create a temporary Foo object that gets copy-constructed into x? How many temporaries? Said another way, does return-by-value necessarily degrade performance?
The point of this FAQ is that the answer is No, commercial-grade C++ compilers implement return-by-value in a way that lets them eliminate the overhead, at least in simple cases like those shown in the previous FAQ. In particular, all(?) commercial-grade C++ compilers will optimize this case:
Foo rbv()
{
...
return Foo(42, 73); © suppose Foo has a ctor Foo::Foo(int a, int b)
}
Certainly the compiler is allowed to create a temporary, local Foo object, then copy-construct that temporary into variable x within yourCode(), then destruct the temporary. But all(?) 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.
You can stop here if you don't want to genuinely understand the previous paragraph, but if you want to know the secret sauce (so you can, for example, reliably predict when the compiler can and cannot provide that optimization for you), the key is to know that compilers usually implement return-by-value using pass-by-pointer. When yourCode() calls rbv(), the compiler secretly passes a pointer to the location where rbv() is supposed to construct the "returned" object. It might look something like this (it's shown as a void* rather than a Foo* since the Foo object has not yet been constructed):
// Pseudo-code
void rbv(void* put_result_here) © Original C++ code: Foo rbv()
{
...
}
// Pseudo-code
void yourCode()
{
struct Foo x;
rbv(&x); © Original C++ code: Foo x = rbv()
...
}
So the first ingredient in the secret sauce is that the compiler (usually) transforms return-by-value into pass-by-pointer. This means that commercial-grade compilers don't bother creating a temporary: they directly construct the returned object in the location pointed to by put_result_here.
The second ingredient in the secret sauce is that compilers typically implement constructors using a similar technique. This is compiler-dependent and somewhat idealized (I'm intentionally ignoring how to handle new and overloading), but compilers typically implement Foo::Foo(int a, int b) using something like this:
// Pseudo-code
void Foo_ctor(Foo* this, int a, int b) © Original C++ code: Foo::Foo(int a, int b)
{
...
}
Putting these together, the compiler might implement the return statement in rbv() by simply passing put_result_here as the constructor's this pointer:
// Pseudo-code
void rbv(void* put_result_here) © Original C++ code: Foo rbv()
{
...
Foo_ctor((Foo*)put_result_here, 42, 73); © Original C++ code: return Foo(42,73);
return;
}
So yourCode() passes &x to rbv(), and rbv() in turn passes &x to the constructor (as the this pointer). That means constructor directly constructs x.
In the early 90s I did a seminar for IBM's compiler group in Toronto, and one of their engineers told me that they found this return-by-value optimization to be so fast that you get it even if you don't compile with optimization turned on. Because the return-by-value optimization causes the compiler to generate less code, it actually improves compile-times in addition to making your generated code smaller and faster. The point is that the return-by-value optimization is almost universally implemented, at least in code cases like those shown above.
Some compilers also provide the return-by-value optimization your function returns a local variable by value, provided all the function's return statements return the same local variable. This requires a little more work on the part of the compiler writers, so it isn't universally implemented; for example, GNU g++ 3.3.3 does it but Microsoft Visual C++.NET 2003 does not:
// Actual C++ code for rbv()
Foo rbv()
{
...
Foo ans = Foo(42, 73);
...
do_something_with(ans);
...
return ans;
}
The compiler might construct ans in a local object, then in the return statement copy-construct ans into the location pointed to by put_result_here and destruct ans. But if all return statements return the same local object, in this case ans, the compiler is also allowed to construct ans in the location pointed to by put_result_here:
// Pseudo-code
void rbv(void* put_result_here) © Original C++ code: Foo rbv()
{
...
Foo_ctor((Foo*)put_result_here, 42, 73); © Original C++ code: Foo ans = Foo(42,73);
...
do_something_with(*(Foo*)put_result_here); © Original C++ code: do_something_with(ans);
...
return; © Original C++ code: return ans;
}
Final thought: this discussion was limited to whether there will be any extra copies of the returned object in a return-by-value call. Don't confuse that with other things that could happen in yourCode(). For example, if you changed yourCode() from Foo x = rbv(); to Foo x; x = rbv(); (note the ; after the declaration), the compiler is required to use Foo's assignment operator, and unless the compiler can prove that Foo's default constructor followed by assignment operator is exactly the same as its copy constructor, the compiler is required by the language to put the returned object into an unnamed temporary within yourCode(), use the assignment operator to copy the temporary into x, then destruct the temporary. The return-by-value optimization still plays its part since there will be only one temporary, but by changing Foo x = rbv(); to Foo x; x = rbv();, you have prevented the compiler from eliminating that last temporary.
---------------------------------------------------------------------------------------------------------------------
What's the "static initialization order fiasco"?
A subtle way to crash your program.
The static initialization order fiasco is a very subtle and commonly misunderstood aspect of C++. Unfortunately it's very hard to detect the errors occur before main() begins.
In short, suppose you have two static objects x and y which exist in separate source files, say x.cpp and y.cpp. Suppose further that the initialization for the y object (typically the y object's constructor) calls some method on the x object.
That's it. It's that simple.
The tragedy is that you have a 50%-50% chance of dying. If the compilation unit for x.cpp happens to get initialized first, all is well. But if the compilation unit for y.cpp get initialized first, then y's initialization will get run before x's initialization, and you're toast. E.g., y's constructor could call a method on the x object, yet the x object hasn't yet been constructed.
I hear they're hiring down at McDonalds. Enjoy your new job flipping burgers.
If you think it's "exciting" to play Russian Roulette with live rounds in half the chambers, you can stop reading here. On the other hand if you like to improve your chances of survival by preventing disasters in a systematic way, you probably want to read the next FAQ.
Note: The static initialization order fiasco can also, in some cases, apply to built-in/intrinsic types.
---------------------------------------------------------------------------------------------------------------------
How can I handle a constructor that fails?
Throw an exception.
Constructors don't have a return type, so it's not possible to use return codes. The best way to signal constructor failure is therefore to throw an exception. If you don't have the option of using exceptions, the "least bad" work-around is to put the object into a "zombie" state by setting an internal status bit so the object acts sort of like it's dead even though it is technically still alive.
The idea of a "zombie" object has a lot of down-side. You need to add a query ("inspector") member function to check this "zombie" bit so users of your class can find out if their object is truly alive, or if it's a zombie (i.e., a "living dead" object), and just about every place you construct one of your objects (including within a larger object or an array of objects) you need to check that status flag via an if statement. You'll also want to add an if to your other member functions: if the object is a zombie, do a no-op or perhaps something more obnoxious.
In practice the "zombie" thing gets pretty ugly. Certainly you should prefer exceptions over zombie objects, but if you do not have the option of using exceptions, zombie objects might be the "least bad" alternative.
---------------------------------------------------------------------------------------------------------------------
What's the deal with destructors?
A destructor gives an object its last rites.
Destructors are used to release any resources allocated by the object. E.g., class Lock might lock a semaphore, and the destructor will release that semaphore. The most common example is when the constructor uses new, and the destructor uses delete.
Destructors are a "prepare to die" member function. They are often abbreviated "dtor".
---------------------------------------------------------------------------------------------------------------------
What's the order that local objects are destructed?
In reverse order of construction: First constructed, last destructed.
In the following example, b's destructor will be executed first, then a's destructor:
void userCode()
{
Fred a;
Fred b;
...
}
---------------------------------------------------------------------------------------------------------------------
Can I overload the destructor for my class?
No.
You can have only one destructor for a class Fred. It's always called Fred::~Fred(). It never takes any parameters, and it never returns anything.
You can't pass parameters to the destructor anyway, since you never explicitly call a destructor (well, almost never).
Should I explicitly call a destructor on a local variable?
No!
The destructor will get called again at the close } of the block in which the local was created. This is a guarantee of the language; it happens automagically; there's no way to stop it from happening. But you can get really bad results from calling a destructor on the same object a second time! Bang! You're dead!
---------------------------------------------------------------------------------------------------------------------
No comments:
Post a Comment