Jonathan Boccara's blog

How to Define A Variadic Number of Arguments of the Same Type – Part 5

Published June 7, 2021 - 0 Comments

Defining a variadic pack of arguments of the same type turns out to be a deep topic as this is the fifth post and seventh technique we (I or guest writer Tobias in Part 4) discover on this topic.

C++ variadic templates allow to define a parameters pack with any number of parameters of any type:

template<typename... Ts>
void myFunction(Ts const&... value)
{
    // ...

But they don’t allow to define a template with any number of parameters of a given type:

template<typename... std::string> // imaginary C++
void myFunction(std::string const&... values)
{
   // ...

However this need comes up when designing interfaces.

In the first four articles on the topic, we focused on defining template parameters of a given type such as std::string, int or MyUserDefinedClass.

My colleague and friend Jonathan asked me how to define a variadic number of parameters of the same type, but with that type being a template parameter, that can be of any type.

Said differently, how can we implement with legal C++ the equivalent of this imaginary C++ code:

template<typename T>
void myFunction(T const&... values) // imaginary C++
{
    // ...

Expressing that all types are the same

One way to define this interface is to introduce a boolean expression that checks if all the types in a template parameters pack are identical. We can then use this boolean with SFINAE to activate the definition of the function (or class) only if that boolean evaluates to true.

But as we’re about to see, it’s not as simple as it seems.

Let’s start by defining the boolean.

Defining AllSame

C++ allows us to compare two types with the std::is_same type traits. One way to compare more than two types is to check that all types are the same as the first type of the pack.

We therefore want to express that the second type is equal to the first one AND that the third one is equal to the first one AND the fourth one is equal to the first one, and so on.

We see from the above sentence that we want to make a conjunction, that is to say a combination of AND conditions. For that we can use C++17 std::conjunction (which we can also emulate in C++11):

template<typename T, typename... Ts>
using AllSame = std::enable_if_t<std::conjunction_v<std::is_same<T, Ts>...>>;

Let’s examine this construct bit by bit:

std::is_same<T, Ts> checks that a given type of the pack Ts is equal to the first type of the pack, T.

std::conjunction_v<std::is_same<T, Ts>...> checks that all types of the pack Ts are equal to T.

std::enable_if_t<std::conjunction_v<std::is_same<T, Ts>...>> is a type that exists if all types of Ts are equal to T, and that is not defined otherwise (check out this post on SFINAE if you’re not familiar with std::enable_if).

Using AllSame with SFINAE

Let’s now use AllSame with SFINAE:

template<typename... Ts, typename = AllSame<Ts...>>
void f(Ts const& values...)
{
}

And the result is that… it doesn’t compile. Here is the output of the compiler when running this code:

<source>:7:47: error: pack expansion argument for non-pack parameter 'T' of alias template 'template<class T, class ... Ts> using AllSame = std::enable_if_t<conjunction_v<std::is_same<T, Ts>...> >'
    7 | template<typename... Ts, typename = AllSame<Ts...>>
      |                                               ^~~
<source>:4:10: note: declared here
    4 | template<typename T, typename... Ts>
      |          ^~~~~~~~
<source>:8:27: error: parameter packs not expanded with '...':
    8 | void f(Ts const& values...)
      |                           ^
<source>:8:27: note:         'Ts'
ASM generation compiler returned: 1
<source>:7:47: error: pack expansion argument for non-pack parameter 'T' of alias template 'template<class T, class ... Ts> using AllSame = std::enable_if_t<conjunction_v<std::is_same<T, Ts>...> >'
    7 | template<typename... Ts, typename = AllSame<Ts...>>
      |                                               ^~~
<source>:4:10: note: declared here
    4 | template<typename T, typename... Ts>
      |          ^~~~~~~~
<source>:8:27: error: parameter packs not expanded with '...':
    8 | void f(Ts const& values...)
      |                           ^
<source>:8:27: note:         'Ts'
Execution build compiler returned: 1

Do you understand what’s going on? Because I don’t.

SFINAE needs an additional parameter

A little change makes this code compile. Here is again the code that didn’t compile:

template<typename... Ts, typename = AllSame<Ts...>>
void f(Ts const& values...)
{
}

And here is a little change that makes it compile:

template<typename T, typename... Ts, typename = AllSame<T, Ts...>>
void f(T const& value, Ts const& values...)
{
}

By separating the first parameter from the rest of the pack, thus mimicking the format of the pack inside AllSame, the code now compiles fine.

Let’s test it. Those two lines of code compile fine because the parameters we pass are of the same type:

f(1, 2, 3);
f("a", "b", "c");

But this one fails to compiles, which is exactly what we wanted:

f(1, "b", 3);

A strange reason

Let’s go back to this fix we made to make the definition of f compile: extracting the first parameter of the pack. It doesn’t seem to make sense. Why should the code using AllSame pass a pack in the form that AllSame uses inside its definition?

Indeed, it seems that the compiler should be able to open up the template pack on its own. We can even argue that this code is detrimental for encapsulation because it makes the user of AllSame depend on one of the implementation aspects of AllSame.

So why? We can find an answer in this Stack Overflow thread. In summary, this is a limitation with alias templates, and we don’t know if and when it will be addressed.

We now know how to define a variadic pack of the same type, for any type (thanks Jonathan for the great question!), and we’ve learned a subtlety on templates along with it.

You will also like

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