Jonathan Boccara's blog

How to Define a Variadic Number of Arguments of the Same Type – Part 1

Published January 25, 2019 - 0 Comments

Daily C++Since C++98, templates have allowed functions to accept parameters of any type:

template<typename T>
void myFunction(T const& x) // T can be of any type
{
    // ...
}

In C++11, variadic templates have allowed functions to accept any number of parameters of any type:

template<typename... Ts>
void myFunction(Ts const&... xs) // the Ts can be of any number of any type
{
    // ...
}

Now how could we make a function accept any number of parameters of the same type? And when would that be useful in business code?

Let’s see one case that has this need, and 3.5 ways to achieve it (yes, you read well, 3.5).

While writing this post I realized that it was longer than I thought at first. For this reason, let’s split it into two parts to make it easier to digest: we’ll see 1.5 ways here and the other two in the next part:

Part 1 – This article:

  • Use case: taking an input in several pieces
  • Solution 0.5: Build it with your bare hands
  • Solution 1: A pinch of SFINAE

Part 2 – The next article:

  • Solution 2: Being static_assertive
  • Solution 3: A little-known feature of C++

EDIT: As indicated by Stefano Bellotti, homogeneous variadic function parameters have been proposed for addition to the standard.

Use case: taking an input in several pieces

To illustrate, let’s consider a function that we want to call with any number of strings:

f("So");
f("So", "long");
f("So", "long", ", and thanks for all the fish");

We have several strings, each coming from a different source and each carrying a part of the information. We’d like to give them all to f, and leave it the trouble to assemble it all.

Actually, it also leaves the liberty to f of assembling it the way it needs: maybe it will simply concatenate those strings together, maybe it will put hyphens between each, or maybe even something different.

Let’s say that in this interface, the message that f means to convey is this: “give me all the strings that constitute the info that you have, and I’ll deal with them”.

On the other hand, we don’t want f to accept values that are not (convertible to) strings. For instance, we don’t want the following code to compile:

f("So", 42, "long");

because of the int that squeezed in the middle.

To implement f, we can’t just stick the “...” operator of the variadic syntax onto std::string:

void myFunction(std::string const&... xs) // imaginary C++!
{
    // ...
}

So, how do we implement f?

Variadic number of arguments of the same type C++

Thanks to Simon Brand for his various pieces of feedback and corrections on the post.

Solution 0.5: Build it with your bare hands

This is not an elegant solution. What’s more, it only solves the problem approximately. For this reason, let’s say that this solution only counts as 0.5. Unfortunately, this is the only one that works with C++98, to my knowledge.

The idea is this: you need several overloads for f? Well, let’s just implement them!

Of course, we can’t implement all the needed overloads, since there is no theoretical limit to their number. But we could set an arbitrary limit. Say 7, for example. Then we would go and write those 7 overloads, that could fall back on one of them depending on the desired implementation of f.

To illustrate, let’s say that f merely concatenates its argument before operating on the result:

void f(std::string const& input)
{
    // do the actual operation on the input
}

void f(std::string const& input1, std::string const& input2)
{
    f(input1 + input2);
}

void f(std::string const& input1, std::string const& input2, std::string const& input3)
{
    f(input1 + input2 + input3);
}

// ...
// same thing with 3, then 4, then 5, then 6 parameters...
// ...

void f(std::string const& input1, std::string const& input2, std::string const& input3, std::string const& input4, std::string const& input5, std::string const& input6, std::string const& input7)
{
    f(input1 + input2 + input3 + input4 + input5 + input6 + input7);
}

As a side note, like we saw in the complete guide to building strings in C++, summing the std::strings this way is not the most efficient way to concatenate them, because it creates a lot of temporary strings in the process. So we would probably implement this part in a more elaborate manner if we want better performance. But let’s stay focused on the variadic number of parameters just now.

Despite being a pain to write and to read, and leading to code duplication, there is at least one advantage to this solution: since it doesn’t use template code, all the implementation of f can stay in a .cpp file and doesn’t have to be exposed in a header. Additionally, it accepts types that are convertible to std::string, such as const char*.

So, in summary:

Advantages of building it with your bare hands:

  • all the implementation in a .cpp file,
  • compatible with C++98,
  • accepts convertible types.

Drawbacks of building it with your bare hands:

  • doesn’t allow any number of parameter, there is an arbitrary limit,
  • a lot of code to say little,
  • duplication of code.

Let’s now move on to solutions implementable in C++11.

Solution 1: A pinch of SFINAE

The solutions in C++11 are based on variadic templates.

The first one consists in using SFINAE to disable all instantiations of f whose parameters are not of the type std::string.

For this, we need to determine two things:

  • the enable_if expression that says that all the types are strings,
  • find a place in the function prototype to fit this enable_if expression.

All types are std::string

To check whether one given type is convertible to std::string we can use the is_convertible type trait, available in the <type_traits> header in C++11:

std::is_convertible<T, std::string>::value

Now that we can check if each parameter is a string, how to we check that all parameters are?

In C++17 we can use the std::conjunction (and even more directly std::conjunction_v, that uses a *_v expression) template:

std::conjunction_v<std::is_convertible<Ts, std::string>...>

Or we could even use a fold expression:

std::is_convertible_v<Ts, std::string> && ...

Now if you don’t have C++17, you can still emulate std::conjunction in C++11. One way is to walk recursively down the variadic pack (but recursion on variadic templates is known to be inefficient – if you see how to do it differently here, please let me know!):

template<class...> struct conjunction : std::true_type { };
template<class B1> struct conjunction<B1> : B1 { };
template<class B1, class... Bn>
struct conjunction<B1, Bn...> 
    : std::conditional<bool(B1::value), conjunction<Bn...>, B1>::type {};

(this is the implementation example on cppreference.com, a tad adapted to be compatible with C++11).

EDIT: It turns out we can implement the conjunction without resorting to variadic templates! Thanks to Simon Brand for pointing it out:

template<bool...> struct bool_pack{};
template<class... Ts>
using conjunction = std::is_same<bool_pack<true,Ts::value...>, bool_pack<Ts::value..., true>>;

So here is how we can express that a variadic pack only contains std::strings in C++11:

conjunction<std::is_convertible<Ts, std::string>...>::value

To use SFINAE, we can put this expression in a std::enable_if:

std::enable_if<std::conjunction<std::is_convertible<Ts, std::string>...>::value>::type;

To make SFINAE look pretty, we can encapsulate this technical expression behind a name, such as AllStrings:

template<typename... Ts>
using AllStrings = typename std::enable_if<std::conjunction<std::is_convertible<Ts, std::string>...>::value>::type;

We can now use the name AllStrings in an enable_if expression.

Where to put the SFINAE

Let’s have a look at our variadic template function:

template<typename... Ts>
void f(Ts const&... xs)
{
    // ...
}

Where do we insert the SFINAE expression? To make SFINAE look pretty, a good choice is usually to use a default template parameter.

template<typename... Ts, typename = AllStrings<Ts...>>
void f(Ts const&... xs)
{
    // ...
}

But isn’t a variadic pack supposed to be the last parameter in a template parameters list? Can there be a default parameter after it?

It turns out there can be, as long as the parameters in the pack are deduced, which is our case here. Indeed, they are deduced thanks to the function parameters.

Could we do SFINAE on each parameter?

In the above interface, it is one global template parameter that carries information about the individual function parameters. Shouldn’t it be the parameters themselves that carry that information? Couldn’t we rather write an interface like this (and wrap the enable_if behind a more meaningful name such as IsString):

template<typename... Ts>
void f(std::enable_if_t<std::is_convertible<Ts, std::string>, Ts> const&... ts)
{
    // ...
}

Well, we can write an interface like this. But the problem is that we can’t call it by passing it std::strings:

f(std::string("hello"), std::string("world")); // oops, no conversion from
                                               // string to enable_if_t<bool, string>

You may think that enable_if_t<bool, string> is std::string in the end. But the compiler hasn’t had the opportunity to figure this out before trying to instantiate the function and failing.

Here is the summary of the pros and cons of solution 1:

Advantages of SFINAE:

  • unlimited number of parameters, as required,
  • the requirement for all strings shows in the interface,

Drawbacks of SFINAE:

  • the implementation of the function template has to be in the header file.

In the next article you’ll see other approaches to this need for a variadic number of arguments of the same type, with different trade-offs.

Stay tuned!

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