Passing containers as out-parameters

Sometimes I see functions that are meant to return a range of values, but instead of returning a container, they take one as out-parameter. There are several reasons why that is not the Right Thing to do.

The basic pattern looks like this:

void getValues(vector<MyValueType>& values) {
  values.clear();
  for (/* some loop conditions */) {
    values.push_back(anotherValue);
  }
}

void someWhereElse() {
  vector<MyValueType> values;
  getValues(values);

  //work with the values
}

The parameter to `getValues` typically is a pure out-parameter, i.e. the function does not care what’s in the container or, like in the example, clears the container just in case.

Sometimes it even gets worse – the above functionality is doubled by a function that does it right, and it is not clear what function is supposed to be used in what circumstances:

vector<MyValueType> getValues()
{
  vector<MyValueType> values;
  getValues(values);
  return values;
}

Why do people think this should be done? To be honest, I am not too sure about that. I have heard that it might be more performant than returning a container and initializing another with that return value.

But what I heard were only rumors, because nobody I heard that statement from was able to provide actual measurements to reinforce the claim.

(Named) Return Value Optimization and copy elision

The RVO, NRVO and copy elision are optimizations made by the compiler that have been around for some time now in most or all major compilers. And with “some time” I don’t mean two or three years. The first implementations of such optimizations have been made in the early 1990s.

Those optimizations are specifically aimed at the up to two copies that in theory would have to be made of an object returned by a function. When they are applied it means, that the compiler does not have to create an object for the variable inside the function and another for the temporary return value. It will perform the construction in place:

vector<MyValueType> getValues() {
  vector<MyValueType> values;        //constructs the myValues object below
  for (/* some loop conditions */) {
    values.push_back(anotherValue);
  }
  return values;                     //no temporary copy here
}

void someWhereElse() {
  vector<MyValueType> myValues = getValues(); //no copy construction here

  //work with the values
}

Trust your compiler when it comes to return value optimization. Never optimize prematurely.

Move semantics

Since C++11 there are rvalue references and move semantics. Any standard library container supports move construction and move assignment, and so should any other container.

With move semantics, the container constructed inside the function will be moved out of the function on the return statement, and the container outside the function will be move constructed or move assigned with the temporary object returned by the function.

So even if your compiler can not apply return value optimization and copy elision for some reason, move semantics will guarantee that no memory allocation is needed and the objects inside the container need not be copied. All that has to be done is usually a few cheap copies of pointers and integers.

Rely on move semantics. If your compiler and library do not support it, get a modern compiler – it really does pay off.

Simplicity and maintainability

Here we go again with the main theme of this blog: Passing the container as an out-parameter is not what would be considered clean code, because it confuses the reader.

People are used to have functions, that get their inputs via arguments and return their outputs via return values. Disguising the return value as parameter is an obfuscation and a hindrance to readability.

So, even if you are stuck with an ancient compiler that supports neither move semantics nor (N)RVO and copy elision, you are often better off returning the container, because readability is much more important than gaining a bit of performance in a place where performance is not critical.

Update:

Of course there are times when passing the container as out-parameter is beneficial and maybe even the only viable option. Such cases include reuse of the container and it’s allocated memory in loops, allocators hidden by type erasure and other cases that are usually done to optimize certain aspects of the porgram.

However, the pass by out-parameter is often done as a default in cases where there is no evidence of a need for optimization and the percieved performance improvement is not as big as thought by implementors.

Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

8 Comments


  1. Taking a container as a parameter means container re-use can remove the allocation overhead. A quick test shows this can be significantly faster.

    Reply
    1. Sai Jagannath

      That applies to the case when you are using the same container / object over and over again like in a loop. Otherwise, there would not be any performance benefits.

      Reply
  2. John

    First a minor point: I doubt every container in the standard library is movable. In particular it would surprise me if std::array was movable, and that is one of my favorite containers (gotta love the stack).

    The bigger performance gains come if the caller already had the container lying around – which is especially relevant to the form that doesn’t call clear() because they may actually want append semantics.

    But I think the bigger issue that both of these approaches suffer from is that they’re tying the caller into a particular container and allocator when the intent of the function is probably just about the values stored inside. I would actually prefer

    template
    void getValues( OutputIterator out )

    That way I can pass you back_inserter(my_vector) or inserter(my_set) or begin(my_array) or whatever is convenient for me and it all works, with the semantics I want that aren’t particularly relevant to getValues().

    And even if I wanted the vector in the first place, there can be very good reasons for me (or more likely a large portion of the application) to select an allocator other than the default one – and quite possibly different than the one your function selects. John Lakos has made some good points about not exposing the allocator type in interfaces.

    Additionally, the iterator version wouldn’t depend on a particular standard library at link time – see all the issues people have had trying to link against Oracle’s OCCI.

    Reply
      1. Keboi

        And what would a move constructor for std::array do exactly?
        Move operations are usually the equivalent of a safe shallow copy (while removing the transferred responsibilities from the moved object). For std::array, a shallow copy and a deep copy are the exact same thing.

        Reply
        1. Arne Mertz

          Depends on the array elements. Array move moves the content, copy copies the content.

          Reply
      2. Anton Bikineev

        The existence of move operations on std::array doesn’t guarrantee them to be as cheap as they are in std::vector (just copying pointer, O(1)). Move operations on std::array move each element in array, i.e. O(n). That’s why I wouldn’t use std::array with fairly large second template parameter as a return type. For detailed answer look at Meyers last book.

        Reply
    1. Arne Mertz

      Thank you for this thorough comment! I agree that there are cases where just returning the container is not appropriate.
      However, there are tons of places where out-parameters for containers are used by default, without custom allocators, performance bottlenecks or append semantics, for “classic” containers like std::vector and std::map.
      People use this as a default just because of a conceived optimization that they have neither proven to need nor to exist in the Form they think it exists.
      I’ll add a clarifying section later today.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *