C++ pass-thru parameters

So in ye olde days, before C++11 and move semantics, it was common for functions to use mutable references to pass container-content to the caller, like this:

void random_between(std::vector<int>& out,
  int left, int right, std::size_t N)
{
  std::uniform_int_distribution<> 
    distribution(left, right);
  for (std::size_t i = 0; i < N; ++i)
    out.push_back(distribution(rng));
}

and you would often use it like this:

std::vector<int> numbers;
random_between(numbers, 7, 42, 10);

Basically trading expressiveness and convenience for speed/efficiency.

Convenience is king

Now obviously, those days are over. With move-semantics and guaranteed copy-elision backing us up, it is usually fine to just return the filled container, like this:

std::vector<int> random_between(int left, int right,
  std::size_t N)
{
  std::vector<int> out;
  std::uniform_int_distribution<>
    distribution(left, right);
  for (std::size_t i = 0; i < N; ++i)
    out.push_back(distribution(rng));
  return out;
}

Now you no longer have to initialize the container to use this function and the function also became pure, clearly differentiating between its inputs and outputs.

Mostly better?

However, there is a downside: Before, the function could be used to append multiple runs into the same container, like this:

std::vector<int> numbers;
for (int i = 0; i < 5; ++i)
  random_between(numbers, 50*i + 7, 50*i + 42, 10);

That use case suddenly became a lot harder. Also, what if you want to keep your vector around and just .clear() it before calling the function again later, to save allocations? That’s also no longer possible. I am not saying that these two use cases should make you prefer the old variant, as they tend not to happen very often. But when they do, it’s all the more annoying. So what if we could have your cake and eat it, too?

A Compromise

How about this:

std::vector<int> random_between(int left, int right,
  std::size_t N, std::vector<int> out = {})
{
  std::uniform_int_distribution<>
    distribution(left, right);
  for (std::size_t i = 0; i < N; ++i)
    out.push_back(distribution(rng));
  return out;
}

Now you can use it to just append again:

std::vector<int> numbers;
for (int i = 0; i < 5; ++i)
  numbers = random_between(
    50*i + 7, 50*i + 42, 10, std::move(numbers));

But you can also use it in the straightforward way, for the hopefully more common case:

auto numbers = random_between(
  50*i + 7, 50*i + 42, 10);

Now you should definitely not do this with all your functions returning a container. But it is a nice pattern to have up your sleeve when the need arises. It should be noted that passing a mutable reference can still be faster in some cases, as that will save you two moves. And you can also add a container-returning facade variant as an overload, but I think this pattern is a very nice compromise that can be implemented by moving a single variable to the parameter list and defaulting it. It keeps 99% of the use cases identically to the original container-returning variant, while making the “append” use slightly more verbose, but also more expressive.

8 thoughts on “C++ pass-thru parameters”

    1. What’s your point? Would you rather I write my filler code using those or use a one of those heavy weight abstractions like a generator when I just want a function returning something. Obviously, you can write random number generation using stdlib primitives without writing those abstractions yourself, but that’s really not the point of my post. Also.. back_inserter? really? The whole article starts with a point about avoiding mutable container references… yuk.

    1. Well, from a clean code perspective you should still prefer functions where all input parameters are actually inputs, and only the return values are outputs. So const-span is fine, but mutable-span is really just a more fine-grained container reference.

  1. Dont you think distribution functions are ecpentive in terms of space when you can use srand() and rand() functions in same way and calculate the range you want

    1. No.
      1) It isn’t easy to reduce range of a random generator. At least if you care about the output distribution. If you just use modulo, then the Pidgeonhole principle will bite you.
      2) rand usually uses linear congruential generator. Its output is very very low quality that isn’t usable for lots of applications such as numerics.

  2. Note that returning a function argument precludes return value optimization.

    See https://en.cppreference.com/w/cpp/language/copy_elision
    In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn’t a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, “named return value optimization”.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.