10

This question was triggered by confusion about RVO in C++11.

I have two ways to "return" value: return by value and return via reference parameter. If I don't consider the performance, I prefer the first one. Since return by value is more natural and I can easily distinguish the input and the output. But, if I consider the efficiency when return large data. I can't decide, because in C++11, there is RVO.

Here is my example code, these two codes do the same work:

return by value

struct SolutionType
{
    vector<double> X;
    vector<double> Y;
    SolutionType(int N) : X(N),Y(N) { }
};

SolutionType firstReturnMethod(const double input1,
                               const double input2);
{
    // Some work is here

    SolutionType tmp_solution(N); 
    // since the name is too long, I make alias.
    vector<double> &x = tmp_solution.X;
    vector<double> &y = tmp_solution.Y;

    for (...)
    {
    // some operation about x and y
    // after that these two vectors become very large
    }

    return tmp_solution;
}

return via reference parameter

void secondReturnMethod(SolutionType& solution,
                        const double input1,
                        const double input2);
{
    // Some work is here        

    // since the name is too long, I make alias.
    vector<double> &x = solution.X;
    vector<double> &y = solution.Y;

    for (...)
    {
    // some operation about x and y
    // after that these two vectors become very large
    }
}

Here are my questions:

  1. How can I ensure that RVO is happened in C++11?
  2. If we are sure that RVO is happened, in nowadays C++ programming, which "return" method do you recommend? Why?
  3. Why there are some library use the return via reference parameter, code style or historical reason?

UPDATE Thanks to these answers, I know the first method is better in most way.

Here is some useful related links which help me understand this problem:

  1. How to return large data efficiently in C++11
  2. In C++, is it still bad practice to return a vector from a function?
  3. Want Speed? Pass by Value.
Community
  • 1
  • 1
Regis
  • 346
  • 1
  • 10

3 Answers3

17

First of all, the proper technical term for what you are doing is NRVO. RVO relates to temporaries being returned:

X foo() {
   return make_x();
}

NRVO refers to named objects being returned:

X foo() {
    X x = make_x();
    x.do_stuff();
    return x;
}

Second, (N)RVO is compiler optimization, and is not mandated. However, you can be pretty sure that if you use modern compiler, (N)RVOs are going to be used pretty aggressively.

Third of all, (N)RVO is not C++11 feature - it was here long before 2011.

Forth of all, what you have in C++11 is a move constructor. So if your class supports move semantics, it is going to be moved from, not copied, even if (N)RVO is not happening. Unfortunatelly, not everything can be semantically moved efficiently.

Fifth of all, return by reference is a terrible antipattern. It ensures that object will be effectively created twice - first time as 'empty' object, second time when populated with data - and it precludes you from using objects for which 'empty' state is not a valid invariant.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • Will it be moved even if in `return x;` `x` is a lvalue and `std::move` is missing? – Zereges May 12 '16 at 13:20
  • 7
    Not a big fan of *fourth*, eh? – IInspectable May 12 '16 at 13:21
  • 4
    @Zereges Never return using `return std::move(something);` It is a pessimization. – NathanOliver May 12 '16 at 13:21
  • @NathanOliver could you provide an explanation? – Zereges May 12 '16 at 13:23
  • 1
    @Zereges using `move` forces the move or copy constructor to be used. If you leave it off then NRVO or RVO can kick in and the returned variable will be directly constructed in the callers space. – NathanOliver May 12 '16 at 13:25
  • 3
    @IInspectable, there are people who avoid 13, Japaneese do not like 6 (if I am not mistaken here), and can I be the one who dislikes 4? :)) JK. Thanks for spotting it. – SergeyA May 12 '16 at 13:25
  • @Zereges, never do std::move from local variable in return statement. It is preventing (N)RVO. – SergeyA May 12 '16 at 13:26
  • @Zereges It is not the type in the function but the type of the return that determines if it is a temporary or not. Even if it is an lvalue in the function if it is returned by value then it is a temporary and can be elided/moved from. – NathanOliver May 12 '16 at 13:30
  • I dont understand Fifth. In the OPs example, `solution` is created only once by the caller, no? – 463035818_is_not_an_ai May 12 '16 at 13:36
  • 2
    @tobi303, what I meant to say in fifth is that you first create your object as an empty object (construction), and than you populate it with data - and this can be also thought of as a construction step (See two-step constructors). This is subefficient, but what is worse, it mandates your class to have a default constructor - which might be semantically inapproriate. – SergeyA May 12 '16 at 13:38
  • @SergeyA : If I add the move constructor of my SolutionType class, is my code more effcient than NRVO version? – Regis May 12 '16 at 13:40
  • @Reigs, in which example? (N)RVO is more efficient than move. – SergeyA May 12 '16 at 13:41
  • @SergeyA: That means my first code example is optimized? – Regis May 12 '16 at 13:44
  • @Reigs, yes, your first example is very likely to perform best on the compilers I worked with. – SergeyA May 12 '16 at 13:47
  • @SergeyA: Cheers! Thanks for your patience. – Regis May 12 '16 at 13:59
  • 1
    @SergeyA You actually *are* mistaken: The Chinese and Japanese both agree with you in disliking 4 (not 6) because it sounds like the word for "death". – Sabre May 12 '16 at 19:04
3

SergyA's answer is perfect. If you follow that advice you almost always won't go wrong.

There is however one kind of 'result' where it is better to pass a reference to the result from the call site.

This is in the case where you are using a std container as a result buffer in a loop.

If you take a look at the function std::getline you'll see an example.

std::getline is designed to fill a std::string buffer from the input stream.

Each time getline is called with the same string reference, the string's data is overwritten. Note that over time (assuming random line lengths), there will sometimes need to be an implicit reserve of the string in order to accommodate new long lines. However, shorter lines than the longest so far will not require a reserve, since there will already be enough capacity.

Imagine a version of getline with the following signature:

std::string fictional_getline(std::istream&);

This implies that a new string returned each time the function is called. Whether or not RVO or NRVO occurred, that string will need to be created and if it's longer than the short string optimisation boundary, this will require a memory allocation. Furthermore, the string's memory will be deallocated each time it goes out of scope.

In this case, and others like it, it is much more efficient to pass your result container as a reference.

examples:

void do_processing(const std::string& s)
{
    // ...
}

/// @post: in the case of an error, os.bad() == true
/// @post: in the case of no error, os.bad() == false
std::string fictional_getline(std::istream& stream)
{
    std::string result;
    if (not std::getline(stream, result))
    {
        // what to do here?
    }
    return result;
}

// note that buf is re-used which will require fewer and fewer 
// reallocations the more the loop progresses
void fast_process(std::istream& stream)
{
    std::string buf;
    while(std::getline(std::cin, buf))
    {
        do_processing(buf);
    }
}

// note that buf is re-created and destroyed each time around the loop    
void not_so_fast_process(std::istream& stream)
{
    for(;;)
    {
        auto buf = fictional_getline(stream);
        if (!stream) break;
        do_processing(buf);
    }
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Thanks for your answer. If i create an object in the loop, i know it will create and distory everytime, but what if i declear the object out of loop? In `not_so_fast_process`, i do it this way: `std::string buf; for(;;){buf=fictional_getline(stream);if (!stream) break;do_processing(buf);}`, does ite re-use `buf` like the buf in `fast_process`? – Regis May 12 '16 at 23:25
  • @Reigs the compiler is allowed to elide a copy constructor regardless of side-effects, but it's not allowed to elide an assignment operator. You'd still have one redundant constructor/destructor cycle per loop in that case. – Richard Hodges May 12 '16 at 23:31
  • I see. If i declear the buf out of loop, in this line `buf=fictional_getline(stream);`, there will be copy all the time due to it is assignment which cannot be elided. So, it will be less efficient. Thank you. – Regis May 12 '16 at 23:41
1

There is no way to ensure that RVO (or NVRO) occurs in C++11. Whether it occurs or not, it is related to quality of implementation (e.g of the compiler), rather than being something fundamentally controllable by the programmer.

Move semantics can be used in some circumstances to achieve a similar effect, but is distinct from RVO.

Generally, I recommend using whatever return method works for the data at hand, that is understandable by the programmer. Code that the programmer can understand is easier to get working correctly. Jiggling with arcane techniques to optimise performance (e.g. in an attempt to force NVRO to occur) tends to make code more difficult to understand, than therefore more likely to have errors (e.g. increased potential for undefined behaviour). If code is working correctly, but MEASUREMENTS show it lacks required performance, then more arcane techniques can be explored to increase performance. But attempting to lovingly hand-optimise code up-front (i.e. before any measurements have provided evidence of a need) is called "premature optimisation" for a reason.

Returning by reference allows avoidance of copying large data on return by a function. So, if the function is returning a large data structure, returning by reference can be more efficient (by various measures) than returning by value. There are trade-offs to this though - returning a reference to something is dangerous (results in undefined behaviour) if the underlying data ceases to exist while some other code has a reference to it. Whereas, returning a value makes it difficult for some code to hold a reference to (for example) a data structure that might have ceased to exist.

EDIT: adding example where returning by reference is dangerous, as requested in comment.

   AnyType &func()
   {
       Anytype x;
        // initialise x in some way

       return x;
   };

   int main()
   {
        // assume AnyType can be sent to an ostream this wah

        std::cout << func() << '\n';     // undefined behaviour here
   }

In this case, func() returns a reference to something that no longer exists after it returns - commonly called a dangling reference. So any use of that reference (in this case, to print the referred value) has undefined behaviour. Returning by value (i.e. simply removing the &) returns a copy of the variable, which exists when the caller attempts to use it.

The cause of undefined behaviour is how func() returns. However, the undefined behaviour will occur in the caller (which uses the reference) not within func() itself. That separation between cause and effect can make bugs that are very difficult to track down.

Peter
  • 35,646
  • 4
  • 32
  • 74
  • Thanks for your answer. i know this, "premature optimization is the root of all evil." And, I'm not try to optimise it. I'm a newbie of C++, when i write code, i just want to write it the right way. Can you give me a example of "returning a reference to something is dangerous"? i think the reference is in the parameter list, it **does** exist. – Regis May 12 '16 at 13:58
  • Okay. I've given an example of a function returning a dangling reference. It is a little more complicated, but still easy, to construct similar examples, where an argument is passed by reference, and that reference is returned, but the referred object ceases ceases to exist before it is used. – Peter May 13 '16 at 09:18
  • Sorry, maybe I didn't discribe the question clearly. In my question, it is not return by reference, it is "return" out via reference parameter, and the real return is void. My appology for misleading. – Regis May 13 '16 at 10:53