Link: https://informationalmind.appspot.com/2012/12/7/FunctionReturningInCpp11.html
I’m writing a lot of cpp11 code recently, and tried some of the features in details. After some trials, I decide to write about an easy but useful aspect of the cpp11 feature: value returning of functions.
Let’s go.
With the new C++11 features, value returning of functions have some new way to do. But let’s review the old way first.
The old way of returning
Before cpp11, returning value of functions have some inconvenience when have big struct of data to return, or multiple value to return. If you want a function return a big struct constructed in function, you may use out parameters. If that struct is returned directly, there is no guarantee return value optimization(RVO) will be done. If you want a function to return multiple value, out parameter is the common choice, or you can define a struct to hold the multiple return value and return that struct by return value or out parameter.
For example, return big data:
struct Big
{
// ...
};
// before cpp11, there is no guarantee to have RVO.
Big ConstructBig()
{
Big b;
// construct Big
return b;
}
// you have to allocate a Big some where else and pass it in.
void ConstructBigByOutParameter(Big* out)
{
if (out)
{
// construct Big
}
}
and some more, multiple return value:
// allocate each parameter's storage some where else and pass them in.
void MultipleReturnByOutParameters(int* i, double* d, Big* b)
{
// test and assign value to each parameters
}
struct ReturnValue
{
int i;
double d;
Big b;
};
// before cpp11, there is no guarantee to have RVO.
ReturnValue MultipleReturnByValueStruct()
{
ReturnValue r;
// assign value to each fields in r
return r;
}
New approaches with C++11
Before continue, make sure you understand rvalue reference and move constructor.
Big struct returning
RVO now is a requirement in cpp11, so the best way to do value returning is return naked local variable or unnamed temporary value directly.
So, just do this:
// in cpp11, this is guaranteed to have RVO
Big ConstructBig()
{
Big b;
// construct Big
return b;
}
// use this as:
Big returned = ConstructBig();
When optimization turned on, complier will construct the variable b at the at returned directly with out any coping.
(When optimization turned off, RVO won’t be done, local variable b will be moved out instead if Big have a move constructor.)
If the object of Big can / need be heap allocate, a better way to do is return by unique_ptr / shared_ptr.
As below:
// if Big can/need be heap allocated, this is an optimized way.
std::unique_ptr<Big> ConstructBigByUniquePtr()
{
auto pb = std::unique_ptr<Big>(new Big());
// construct Big
return pb;
}
And, don’t do it like this, this will cause resource leak when your project grow large:
// DON'T do this.
Big* ConstructBigByPtr()
{
auto pb = new Big();
// construct Big
return pb;
}
Multiple value returning
Return by out parameters is still a choice, but now we have some more elegant way, though it may not be the most optimized. That is, using tuple (or pair):
// return multiple return in a tuple
std::tuple<int, double, Big> MultipleReturnByTuple()
{
std::tuple<int, double, Big> t;
// construct each value
return t;
// or return std::make_tuple(intValue, doubleValue, std::move(bigValue));
}
Using tuple to wrap multiple value together and return, and then, we still have two way to retrieve these values.
Store the returned tuple and retrieve each value when use:
std::tuple<int, double, Big> t = MultipleReturnByTuple();
int i = std::get<0>(t); // using int
double d = std::get<1>(t); // using double
Big& b = std::get<2>(t); // using Big
Or using tie to retrieve each value when get the return value:
int i;
double d;
Big b;
std::tie(i, d, b) = MultipleReturnByTuple();
// now we can use them
Here are something worth noting when using tuple to do returning:
- In MultipleReturnByTuple, no matter which way you construct the tuple, these values in the tuple are copied / moved into it, and then returned to outside. (If Big have a default constructor, tuple can be default constructed, but the content of the default constructed Big object is threw away when copy / move a new Big object into the tuple. So, prefer the committed out return statement.)
- When receive the tuple as the first approach, RVO will be done, so no extra copy when function return.
- When receive the tuple value as the second approach, RVO cannot apply to it, each value will be moved into corresponding one.
So when using the first receiving approach, Big will be copied / moved once, but using the second approach, Big will be copied / moved twice.
When not to use tuple to return multiple value
If some of the return value are optional for the caller, and calculate them are not cheap, prefer the old out parameter approach so the function can check each out pointer and skip the calculation if it’s nullptr.
Miscellaneous
RVO won’t apply if returning a member of a struct. You can move it out explicitly if it can be moved. If that is a big struct and cannot be moved, do not return it that way. Using return by unique_ptr / shared_ptr may be a better choice.
For example:
struct Processor
{
Big result;
void ConstructResult();
};
Big ConstructBigUsingProcessor()
{
Processor p;
p.ConstructResult();
// RVO cannot apply if returned like this.
// If Big cannot be moved, this will copy the whole result out.
return std::move(p.result);
}
Closing
New features in C++11 make the function return more elegant, if used properly, it can make the code easier to write, easier to read. But pay attention to the performance under it especially when using tuple to return big structure.