views:

905

answers:

7

Short version: It's common to return large objects—such as vectors/arrays—in many programming languages. Is this style now acceptable in C++0x if the class has a move constructor, or do C++ programmers consider it weird/ugly/abomination?

Long version: In C++0x is this still considered bad form?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

The traditional version would look like this:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

In the newer version, the value returned from BuildLargeVector is an rvalue, so v would be constructed using the move constructor of std::vector, assuming (N)RVO doesn't take place.

Even prior to C++0x the first form would often be "efficient" because of (N)RVO. However, (N)RVO is at the discretion of the compiler. Now that we have rvalue references it is guaranteed that no deep copy will take place.

Edit: Question is really not about optimization. Both forms shown have near-identical performance in real-world programs. Whereas, in the past, the first form could have had order-of-magnitude worse performance. As a result the first form was a major code smell in C++ programming for a long time. Not anymore, I hope?

+23  A: 

Dave Abrahams has a pretty comprehensive analysis of the speed of passing/returning values.

Short answer, if you need to return a value then return a value. Don't use output references because the compiler does it anyway. Of course there are caveats, so you should read that article.

Peter Alexander
Although Dave doesn't mention which compilers he tested AFAICS.
Justicle
Thanks. That article has almost the exact same example that I posted.
Nate
"compiler does it anyway": compiler isn't required to do that == uncertainty == bad idea (need 100% certainty). "comprehensive analysis"There is a huge problem with that analysis - it relies on undocumented/non-standard language features in unknown compiler ("Although copy elision is never required by the standard"). So even if it works, it is not a good idea to use it - there is absolutely no warranty that it will work as intended, and there is no warranty that every compiler will always work this way. Relying on this document is a bad coding practice, IMO. Even if you'll lose performance.
SigTerm
@SigTerm: That is an excellent comment!!! most of the referenced article is too vague to even consider for use in production. People think anything an author who's written a Red In-Depth book is gospel and should be adhered to without any further thought or analysis. ATM there isn't a compiler on the market that provides copy-elison as varied as the examples Abrahams uses in the article.
Hippicoder
@SigTerm, there's a **lot** that the compiler is not required to do, but you assume it does anyway. Compilers aren't "required" to change `x / 2` to `x >> 1` for `int`s, but you assume it will. The standard also says nothing about how compilers are required to implement references, but you assume that they are handled efficiently using pointers. The standard also says nothing about v-tables, so you can't be sure that virtual function calls are efficient either. Essentially, you need to put some faith in the compiler at times.
Peter Alexander
@Peter Alexander: "x / 2 to x >> 1 for `int`s, but you assume it will", "but you assume that they are handled using pointers" actually, I don't assume that. It is *unknown* what compiler will do, and although it is *possible* that compiler will do what you think, it is not *guaranteed*, so it will be safer to assume that compiler will do exactly opposite thing(Murphy's law). "you need to put some faith" I don't. I prefer to "stand on solid ground". Assuming too much in programming is like juggling with live grenades - sometimes it works fine, but when it doesn't, results are catastrophic.
SigTerm
@Peter Alexander: "you need to put some faith in the compiler at times". I can't put my faith into all existing compilers at once. And I don't want to be chained to a single development tool or platform.
SigTerm
@Sig: Very little is actually guaranteed except the actual output of your program. If you want 100% certainty about what is going to happen 100% of the time, then you're better off switching to a different language outright.
Dennis Zickefoose
@Dennis Zickefoose: "Very little is actually guaranteed except the actual output of your program." This is with me.
SigTerm
@SigTerm. It is not "unknown" what compilers do. It is merely unspecified by the standard. We know very well what compilers do in many cases, and the optimisations they perform (through empirical tests). If you only go by what the standard requires then I suppose you use `export` in all your programs (because the standard requires that... right?). No, of course not. You learn what your compiler(s) can do and you take that into account when you are programming. It's just common sense.
Peter Alexander
@Peter Alexander: "unspecified" == you're not certain == "unknown". I still believe that relying on document you mentioned is a very BAD idea. In my experience, when you think that "compiler will optimize thing in certain way", compiler frequently does not behave as you expected. You can rely on stl, and you can assume that it will work as you expected on any platform/compiler. BUt assuming that compiler will not call a copy constructor when it should is extremely unwise - if you change compiler and there is no RVO/elision, ALL your code will go to hell, and you'll have to rewrite everything.
SigTerm
My faith is the amount of trust I have in the compilers I'm using proportional to the amount of additional work it would require if I distrust them. faith = trust in compiler/amount of work to distrust. When that's greater than or equal to 1, I go with the compiler and hope it optimizes things for me.
@Peter Alexander: I.e. in the end it is very unwise to rely on RVO/copy elision, unless ALL C++ compilers are __required__ to implement them. I prefer to keep my sanity and avoid rewriting everything on the day when "undocumented feature" stops working. If it is undocumented - it may be changed. "If something may go wrong, it will", so you can assume worst-case scenario. This is what I call common sense.
SigTerm
@Peter Alexander: When I return by value, or pass arguments by value, I expect compiler to call copy constructor and waste CPU. When RVO/elision is in effect, it will perform better than I expected. On contrary, when you rely on RVO/elision, and they are not implemented, program will perform worse than you expected. So, if I assume "worst-case scenario"(there is no RVO), programm may work faster than I though. If you assume best-case scenario(there is RVO), program may work slower than you thought. I may get pleasant surprise, you may get unpleasant one. So, not relying on RVO is safer.
SigTerm
@SigTerm: I work on "actual-case scenario". I test what the compiler does and work with that. There is no "may work slower". It simply does not work slower because the compiler DOES implement RVO, whether the standard requires it or not. There are no ifs, buts, or maybes, it's just simple fact.
Peter Alexander
@Peter Alexander: If you want to use undocumented language features, you can do that but ONLY if they are documented compiler features. I.e. there is a documentation provided by compiler developer (not by a blog in the middle of nowhere) that states when and how compiler uses RVO/elision. You should expect than this feature will be removed in the next release or that your company will decide to switch to compiler without that feature. "compiler DOES implement RVO" Wrong. "*some* compilers implement RVO", and there is no warranty that all of them behave in this way.
SigTerm
@Peter Alexander: I trust (more or less) to official documentation only. This is what I meant when I said about "standing on solid ground". I.e. C++ standard or information provided by compiler developer (microsoft, GCC devs, etc).YOur "analysis" cites neither of them. Which means it is a bad advice relying on something that "works" for unknown reason and bound to stop working in the future. Even tests are not always a good solution, because you're prone to human errors - you can miss a situation when undocumented feature will misbehave. I believe this is the end of discussion.
SigTerm
It is documented by the compilers. Go have a look for yourself.
Peter Alexander
excellent link.
Alexandre C.
+1  A: 

If performance is a real issue you should realise that move semantics aren't always faster than copying. For example if you have a string that uses the small string optimization then for small strings a move constructor must do the exact same amount of work as a regular copy constructor.

Motti
@Motti: NRVO doesn't go away just because move constructors were added.
Billy ONeal
@Billy, true but irrelevant, the question was has C++0x changed the best practices and NRVO hasn't changed due to C++0x
Motti
+6  A: 

Just to nitpick a little: it is not common in many programming languages to return arrays from functions. In most of them, a reference to array is returned. In C++, the closest analogy would be returning boost::shared_array

Nemanja Trifunovic
True. Behind the scenes it's dealing with references, but in a language like Python or PHP or some variants of BASIC that detail is hidden from you.
Nate
A `std::vector` is like a reference to an array -- the pointer to the actual array block is stored the vector object. That pointer is going to be move constructed (C++0x)/NRVO'd (C++03) for the calling function, and therefore it's no different than passing a reference.
Billy ONeal
@Billy: std::vector is a value type with copy semantics. The current C++ standard offers no guarantees that (N)RVO ever gets applied, and in practice there are many real-life scenarios when it is not.
Nemanja Trifunovic
@Nemanja: Yes, the standard does not require it, but most compilers in modern use are going to do it.
Billy ONeal
@Billy: Again, there are some very real scenarios where even the latest compilers don't apply NRVO: http://www.efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic
@Nemanja: That does not change the fact that in 99% of cases, you can treat the returned vector as if you returned a reference type. If you profile and see a lot of copies from returns then you can think about changing that sort of thing.
Billy ONeal
@Billy ONeal: 99% is not enough, you need 100%. Murphy's law - "if something can go wrong, it will". Uncertainty is fine if you're dealing with some kind of fuzzy logic, but it is not a good idea for writing traditional software. If there is even 1% of possibility that code does not work the way you think, then you should expect this code will introduce critical bug that will get you fired. Plus it is not a standard feature. Using undocumented features is a bad idea - if in one year from know compiler will drop feature (it isn't _required_ by standard, right?), you'll be the one in trouble.
SigTerm
@SigTerm: If we were talking about correctness of behavior, I would agree with you. However, we are talking about a performance optimization. Such things are fine with less than 100% certainty.
Billy ONeal
@Billy: How did you come up with the 99% number? Anyway, even if I profile it with one compiler it does not mean another compiler will perform the same optimizations. In a nutshell, I would say that relying on RVO is safe enough these days, but not NRVO.
Nemanja Trifunovic
@Nemanja: I don't see what's being "relied upon" here. Your app runs the same no matter if RVO or NRVO is used. If they're used though, it will run faster. If your app is too slow on a particular platform and you traced it back to return value copying, then by all means change it, but that does not change the fact that the best practice is still to use the return value. If you absolutely need to ensure no copying occurs wrap the vector in a `shared_ptr` and call it a day.
Billy ONeal
@Billy: The app may or may not "run the same" - if the vector is big enough it may lead to unacceptable performance or even a crash. Of course, there are cases when you are certain the vector content is small enough not to cause any such problems, but in general I wouldn't just copy vectors blindly and hope that complier(s) perform copy elision for me.
Nemanja Trifunovic
+15  A: 

At least IMO, it's usually a poor idea, but not for efficiency reasons. It's a poor idea because the function in question should usually be written as a generic algorithm that produces its output via an iterator. Almost any code that accepts or returns a container instead of operating on iterators should be considered suspect.

Don't get me wrong: there are times it makes sense to pass around collection-like objects (e.g., strings) but for the example cited, I'd consider passing or returning the vector a poor idea.

Jerry Coffin
Excellent insight.
Nate
The problem with the iterator approach is it requires you to make functions and methods templated, even when the collection element type is known. This is irritating, and when the method in question is virtual, impossible. Note, I'm not disagreeing with your answer per se, but in practice it just becomes a bit cumbersome in C++.
jon hanson
I have to disagree. Using iterators for output is sometimes appropriate, but if you aren't writting a generic algorithm, generic solutions often provide unavoidable overhead that is hard to justify. Both in terms of code complexity and actual performance.
Dennis Zickefoose
@Dennis: I have to say my experience has been quite the opposite: I write a fair number of things as templates even when I know the types involved ahead of time, because doing so is simpler and improves performance.
Jerry Coffin
I personally return a container. The intent is clear, the code is easier, I don't care much for the performance when I write it (I just avoid early pessimization). I am unsure whether using an output iterator would make my intent clearer... and I need non-template code as much as possible, because in a large project dependencies kill development.
Matthieu M.
Doing so *might* be simpler, and it *might* improve performance. I posit that, in the case that you are conceptually building a container rather than writing to a range, there is no way changing the interface to operate on iterators instead of vectors will be both. Sure, the implementation of the routine will probably be simplified, but you do so by putting the onus on the caller to properly construct the container in advance. They do it right, and the code is less simple. They do it naively, and the code is less efficient. They do it wrong, and the code is less correct.
Dennis Zickefoose
@Dennis: I will posit that conceptually, you should *never* be "building a container rather than writing to a range." A container is just that -- a container. Your concern (and your code's concern) should be with the contents, not the container.
Jerry Coffin
A: 

Code cleanly, then optimize later, or possibly never.

John
No, programmers still need to know the performance implications of various techniques, which is what this question is about.
Justicle
I agree with @Justicle: If you don't know the performance implications of various techniques, how will you know how to code "cleanly"?
jmucchiello
Really my question wasn't about optimization as much as code style. The "newer" code style was so horribly inefficient at some point in the past that some C++ programmers still cringe when they see it. Now it generally won't matter one way or the other, performance-wise. The two options I showed have different performance characteristics, but compared to the old copy-everything-multiple-times approach, it's irrelevant. Coding style/API design style is more important.
Nate
Nate, your question was very directly about optimization. Why do you think one approach might be ugly/bad/abomination? Why would "large objects" matter? Why do you suspect developers would cringe? Why would every answer so far have directly addressed performance? If there was no assumed performance difference then you would return all "return values" by value and you never would have asked this question in the first place.If we are to believe that your question really is about code style, not performance, then by all means, give a few -1's to all of the responses thus far.
John
Hi John. I did not give you a -1. I will update the question to make it clear that I don’t care about optimization. You said: “If there was no assumed performance difference then… you never would have asked this question in the first place.” *There is no meaningful performance difference* between the two examples, but I did ask the question. After decades of writing C++ code it is seared into my brain that returning large objects is a major code smell, third rail, bad. Maybe younger programmers don’t have that reaction, so there is a mismatch between what I asked and what you thought I asked.
Nate
+4  A: 

I still think it is a bad practice but it's worth noting that my team uses MSVC 2008 and GCC 4.1, so we're not using the latest compilers.

Previously a lot of the hotspots shown in vtune with MSVC 2008 came down to string copying. We had code like this:

String Something::id() const
{
    return valid() ? m_id: "";
}

... note that we used our own String type (this was required because we're providing a software development kit where plugin writers could be using different compilers and therefore different, incompatible implementations of std::string/std::wstring).

I made a simple change in response to the call graph sampling profiling session showing String::String(const String&) to be taking up a significant amount of time. Methods like in the above example were the greatest contributors (actually the profiling session showed memory allocation and deallocation to be one of the biggest hotspots, with the String copy constructor being the primary contributor for the allocations).

The change I made was simple:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

Yet this made a world of difference! The hotspot went away in subsequent profiler sessions, and in addition to this we do a lot of thorough unit testing to keep track of our application performance. All kinds of performance test times dropped significantly after these simple changes.

Conclusion: we're not using the absolute latest compilers, but we still can't seem to depend on the compiler optimizing away the copying for returning by value reliably (at least not in all cases). That may not be the case for those using newer compilers like MSVC 2010. I'm looking forward to when we can use C++0x and simply use rvalue references and not ever have to worry that we're pessimizing our code by returning complex classes by value.

[Edit] As Nate pointed out, RVO applies to returning temporaries created inside of a function. In my case, there were no such temporaries (except for the invalid branch where we construct an empty string) and thus RVO would not have been applicable.

+1 one for actual compiler results
Justicle
That's the thing: RVO is compiler-dependent, but a C++0x compiler *must* use move semantics if it decides not to use RVO (assuming there's a move constructor). Using the trigraph operator defeats RVO. See http://cpp-next.com/archive/2009/09/move-it-with-rvalue-references/ which Peter referred to. But your example is not eligible for move semantics anyway because you're not returning a temporary.
Nate
@Nate: +1 That's a good point about temporaries!
@Stinky472: Returning a member by value was always going to be slower than reference. Rvalue references would still be slower than returning a reference to the original member (if the caller can take a reference instead of needing a copy). In addition, there are still many times that you can save, over rvalue references, because you have context. For example, you can do String newstring; newstring.resize(string1.size() + string2.size() + ...); newstring += string1; newstring += string2; etc. This is still a substantial saving over rvalues.
DeadMG
@DeadMG a substantial saving over binary operator+ even with C++0x compilers implementing RVO? If so, that's a shame. Then again that makse sense since we still end up having to create a temporary to compute the concatenated string whereas += can concatenate directly to newstring.
How about a case like: string newstr = str1 + str2; On a compiler implementing move semantics, it seems like that should be as fast as or even faster than: string newstr; newstr += str1; newstr += str2; No reserve, so to speak (I'm assuming you meant reserve instead of resize).
@Nate: I think you are confusing *trigraphs* like `<::` or `??!` with the *conditional operator* `?:` (sometimes called the *ternary operator*).
FredOverflow
@FredOverflow: Oh yeah. :-)
Nate
@stinky472: For just two strings, it's no different. For more, it's substantial, especially if you want a system that performs acceptably on C++03 compilers. In my opinion, compilers should have the freedom to do whatever they want with rvalues. If the Standard would slacken the rules, we might be able to do even better.
DeadMG
+6  A: 

The gist is:

Copy Elision and RVO can avoid the "scary copies" (the compiler is not required to implement them, and in some situations it can't be applied)

C++ 0x RValue references allow a string/vector implementation that guarantees that.

If you can abandon older compilers / STL implementations, return vectors freely (and make sure your own objects support it, too). If your code base needs to support "lesser" compilers, stick to the old style.

Unfortunately, that has major influence on your interfaces. If C++ 0x is not an option, and you need guarantees, you might use instead reference-counted or copy-on-write objects in some scenarios. They have downsides with multithreading, though.

(I wish just one answer in C++ would be simple and straightforward and without conditions).

peterchen