C++ small dynamic array/vector returns

Started by
20 comments, last by Aressera 7 years, 1 month ago

Code up how you want, until you prove it's a bottleneck do not start to prematurely optimize. You will learn with experience where you can without thinking about things. But you need a lot of experience first. Just write code.

"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

Advertisement

I see, that's quite clever. Thank-you for the explanation Hodgman. Is this your preferred method of returning dynamic data from functions?

Not that it really matters, but I can't for the life of me figure out why this comment would warrant a thumb down...

Is this your preferred method of returning dynamic data from functions?

Yes, but my code is already littered with Scope objects everwhere, so it's a very natural fit.
If I was working on a different code-base, I'd probably just return a std::vector by value and trust in the RVO... or take a std::vector by reference as the output location, e.g.


void GetData( std::vector<int>& results )
{
 for( int i=0; i!=10; ++i ) results.push_back(i);
}

This is nice because it gives the user a little more control. If they're going to be collecting a bunch of different data in a loop, they can clear their vector and reuse the same memory each time, reducing the number of allocations. The user may also want to concatenate multiple sets of results together, which they can do by simply not clearing their vector between each call.
i.e. The best choice also depends on the specifics of the use case.

I almost always use (1) or (4), because its more purely functional form results in the cleaner flow of data through your code. Consider which one of these you'd rather write as a user:


auto result = Foo(Bar1(x), Bar2(y), Bar3(z));

or:


Bar1ResultT bar1;
Bar1(x, bar1);

Bar2ResultT bar2;
Bar2(y, bar2);

Bar3ResultT bar3;
Bar3(z, bar3);

FooResultT result;
Foo(bar1, bar2, bar3, result);

That being said, (2) is useful in cases when you want to use the return value to indicate success/failure (if the results are sometimes unavailable or invalid), or there's some other associated state that comes along with them. But to me it's a more difficult interface to use, so there has to be a specific reason to choose it -- it's never the default.

Consider which one of these you'd rather write as a user:

Well, to be perfectly honest neigther of those are really optimal, I would still rather write a mixup of both:


const auto bar1 = Bar1(x);
const auto bar2 = Bar2(y);
const auto bar3 = Bar3(z);

const auto result = Foo(bar1, bar2, bar3);

Mostly for easier debugging by being able to directly step into Foo with just one keypress. But I agree with your point, I personally try to the reference-out approach as much as possible, since it adds lots of complexety without many gains in most places.

Slightly contrarian view: I prefer #2 to #1, and definitely over #4.

#2 is very much a common C/C++ pattern. Pass in the output object (or struct in C) and let the function fill it out. It looks a little weird at first but experienced programmers will understand exactly what's going on. Probably even more so than #1, and definitely more so than #4.

#2 removes any confusion about where the allocation takes place, who is responsible for it, etc. The caller has control over whether the object gets reused, and that decision can easily be refactored at some later point without changing the function at all.

Now, it's not all pretty and functional looking. But, this is C++ we're talking about. If you wanted pretty, you came to the wrong place.

Sorry to disagree. I mostly just wanted to point out that #2 is common C/C++ and you'll run into it all over the place.

Geoff

#2 removes any confusion about where the allocation takes place, who is responsible for it, etc. The caller has control over whether the object gets reused, and that decision can easily be refactored at some later point without changing the function at all.

Actually, I'd heavily disagree with that part. #2 Actually adds a lot of confusion about what happens to the object you pass in. The callee might do all sorts of it: Clear it, not clear it, append items to it, remove items from it, reserve() memory of it, force_to_shrink it, std::swap it with another vector, etc... so all sorts of things, not only on such a syntactical, but also on more functional level (is the vector supposed to be cleared by the callee? is it already cleared by the caller? Do you call reserve(), because you are the only one filling it at one point, or are multiple consecutive calls happening so this would be harmful? ...) which makes the actual purpose of the function pretty much ambigous, no matter your level of experience in C++.

In #1 on the other hand, there's only one logical, if not possible purpose: The function creates a new collection of objects and returns it, period. So unless you are overly familiar with #2, I'd say #1 should be more understandably. #2's main purpose is to provide potential speedups, otherwise I personally tend to only use it if I want to actually append to an array via multiple unrelated calls.

Sorry to disagree. I mostly just wanted to point out that #2 is common C/C++ and you'll run into it all over the place.

No need to apologice for disagreeing with anything!

I'd argue that generally, there's lot of legacy code like #2 floating around, with people claimings its actually easier to understand, without being actually wrong about it - you're just common to it, like you said, its common C++-code, but from a more objective standpoint, I'd often rather prefer newer approaches.

But, this is C++ we're talking about. If you wanted pretty, you came to the wrong place.

Still, C++XX did a lot to simplify the language and make it actually prettier - while its certainly not C++' selling feature, there's nothing wrong with it eigther - I mean, the point of C++ is not to be as ugly as possible, is it? :)

I mean, the point of C++ is not to be as ugly as possible, is it? :)

You know, every time I do template meta-programming I start to ask myself that very question...

Actually, I'd heavily disagree with that part. #2 Actually adds a lot of confusion about what happens to the object you pass in. The callee might do all sorts of it: Clear it, not clear it, append items to it, remove items from it, reserve() memory of it, force_to_shrink it, std::swap it with another vector, etc... so all sorts of things, not only on such a syntactical, but also on more functional level (is the vector supposed to be cleared by the callee? is it already cleared by the caller? Do you call reserve(), because you are the only one filling it at one point, or are multiple consecutive calls happening so this would be harmful? ...) which makes the actual purpose of the function pretty much ambigous, no matter your level of experience in C++.

Agreed. I also tend to find #2 to be the hardest to maintain of the options. Every pointer- or reference-taking method is a rabbit-hole that must be followed to fully understand how your data is being manipulated, and such methods are often improperly reused or extended to meet the (potentially competing) needs of multiple users.

-.-

Just return the vector normally in 99.9% of cases. If you have a rough idea about how large it's going to be then use .reserve() to avoid extra allocations. Returning a vector by value doesn't cost an additional allocation. It will either be constructed in the return position via NRVO or it will be moved out of the function rather than copied (Do not use std::move() to return it. The compiler knows what it's doing.). The only exception to this is if the vector being returned doesn't match the return type, which... Well, you're on your own there.

If you can demonstrate with a profiler that you're bottlenecked on the vector allocations then you can sanely hand in a vector for re-use, but in order for that to happen you'd pretty much have to be just crapping out vectors in a loop and maybe storing them somewhere. You're always going to be doing some kind of work on that data, aren't you? Maybe just spit out the data and then work on it before spitting out the next vector? That gives the CPU more complexity to work with, so it can parallelize better. (It can be doing all kinds of work while waiting on memory latency.)

Don't do #4. Handing out references to private parts is the same as making them public parts. It's far too easy to spit out some data, get halfway through working on it, then run the function again and completely change the contents of the vector.

Most importantly, stop asking for a "preferred" method. The preferred method is the simplest one that does what you need. I'm having nightmares about some crazy code requirement around return values imposed in order to solve a non-existent problem.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

This topic is closed to new replies.

Advertisement