Jonathan Boccara's blog

How to Pass a Variadic Pack as a First Argument of a Function in C++

Published January 22, 2021 - 0 Comments

I originally wrote this post for Bartek’s blog. Here is a link to the original post.

In C++, the normal way of passing a variadic pack of parameters is at the last position of the function:

template<typename A, typename... Bs>
void f(A&& a, Bs&&... bs)
{
    // implementation of f...
}

But what if the last position doesn’t make sense for the meaning of your function?

Expressive order of parameters

The order of parameters in a function interface carries meaning about what the function is doing. Indeed, there are several logical orders that make sense for a function’s parameters.

One of them, suggested in Code Complete, is in,inout,out: first the inputs of the function, then the parameters that the function reads and modifies (if any), and last the outputs of the function (the objects that the function modifies without reading).

Let’s say that we’d like to follow this convention, but that the inputs are in the variadic pack. This interface is then the wrong way around:

template<typename Output, typename... Inputs>
void f(Output& output, Inputs const&... inputs)
{
    // implementation of f...
}

In this case, we force the callers to pass the inputs last and the outputs first:

f(output, input1, input2, input3);

But we’d rather have a call site that looks like this:

f(input1, input2, input3, output);

I encountered this need in the pipes library, with the send function. send can send any number of values to a pipeline:

send(1, 2, 3, pipeline);

Since 1, 2 and 3 are coming into the pipeline, to me the above call site reads like: “send 1, 2, and 3 to pipeline. This is more expressive than if the variadic pack was located at the end:

send(pipeline, 1, 2, 3);

Since the normal way in C++ is to have the variadic pack at the end, how do we turn around the function parameters to let the caller pass the variadic pack first?

Passing the variadic pack first

To pass the variadic pack first we can use the following trick: wrap the function with the “technical” interface (variadic pack at the end) with another one that you can call with the “logical” interface (variadic pack at the beginning).

We’ll proceed in three steps:

  • Receiving all function parameters (the “logical” interface), starting with the inputs
  • Extracting the inputs and the output
  • Calling the function with outputs first (the “technical” interface)

Receiving all parameters

Since we can’t pass the variadic pack first, we’re going to have one variadic pack containing all the parameters, starting with the inputs and followed by the output:

// usage: f(input1, input2, input3, output);
template<typename... InputsThenOutput>
void f(InputsThenOutput&&... inputsThenOutput)
{

We can add a comment like the one above to clarify how the interface should be called.

This interface doesn’t separate out its parameters. This is a drawback, but one that will allow to have the call site we want. We’ll discuss later on whether this technique is worth the change of interface.

Let’s implement this function. It wraps its arguments into a tuple of references, and indicates where the inputs are located: in our case at all the positions except the last one:

// usage: f(input1, input2, input3, output);
template<typename... InputsThenOutput>
void f(InputsThenOutput&&... inputsThenOutput)
{
    f(std::forward_as_tuple(inputsThenOutput...), std::make_index_sequence<sizeof...(inputsThenOutput) - 1>{});
}

std::forward_as_tuple constructs the tuple of references to the function arguments, so we can pass them on. And std::make_index_sequence constructs a list of indexes from 0 to its parameter.

Extracting the inputs and outputs

Now we have a tuple with all the inputs followed by the output, and a list of indexes indicating the positions of the inputs.

We can easily find the position of the output: it’s the last one. We can then extract this output from the tuple, then extract the inputs, and call the “technical” version of f with the outputs first:

template<typename... InputsThenOutput, size_t... InputIndexes>
void f(std::tuple<InputsThenOutput...> inputsThenOutputs, std::index_sequence<InputIndexes...>)
{
    auto constexpr OutputIndex = sizeof...(InputsThenOutput) - 1;
    fOutputFirst(std::get<OutputIndex>(inputsThenOutputs), std::get<InputIndexes>(inputsThenOutputs)...);
}

Implementing the function

fOutputFirst is the function that has does the real job, because it has access to the individual its parameters (but has the variadic pack at the end):

template<typename Output, typename... Inputs>
void fOutputFirst(Output& output, Inputs const&... inputs)
{
    // implementation of f...
}

We can also keep calling it f and put it in another namespace, as we see in the recap just below.

Putting it all together

Here is all the code together, if you’d like to copy it and adapt it for your need. The first two functions are not supposed to be called directly, so we can put them into another namespace to make this clearer:

namespace detail
{
    template<typename Output, typename... Inputs>
    void f(Output& output, Inputs const&... inputs)
    {
        // implementation of f
    }
    
    template<typename... InputsThenOutput, size_t... InputIndexes>
    void f(std::tuple<InputsThenOutput...> inputsThenOutputs, std::index_sequence<InputIndexes...>)
    {
        auto constexpr OutputIndex = sizeof...(InputsThenOutput) - 1;
        detail::f(std::get<OutputIndex>(inputsThenOutputs), std::get<InputIndexes>(inputsThenOutputs)...);
    }
}

// usage: f(input1, input2, input3, output);
template<typename... InputsThenOutput>
void f(InputsThenOutput&&... inputsThenOutput)
{
    detail::f(std::forward_as_tuple(inputsThenOutput...), std::make_index_sequence<sizeof...(inputsThenOutput) - 1>{});
}

Optimizing for expressiveness

This technique optimizes the expressiveness of the call site at the expense of the one of the interface and implementation. Indeed, the interface needs naming and a comment to help clarify how to use it, and the implementation has more code to turn the parameters around.

Is it worth it? If the function is called at many places in the code and if the parameter order makes more sense, then it can be worth considering applying this technique. I think the send function of the pipes library is such a case, for example.

To decide in the general case, you have to weigh to pros and the cons, and identify what part of the code you want to make the most expressive.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin