Jonathan Boccara's blog

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

Published January 29, 2019 - 0 Comments

Variadic number of arguments of the same type C++

Daily C++How can we define a function that takes any number of arguments of the same type?

This is what we tackled in our previous post: How to Define A Variadic Number of Arguments of the Same Type – Part 1.

C++98 templates allow a function to accept any type, C++11 variadic templates allow it to accept any number of any type, but how to allow it to take any number of a given type?

Indeed, we can’t just write a function like this:

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

We saw the use case of a function that takes its output in several pieces. That functions means to express: “give me all the strings that you want, and I’ll deal with them”.

As we saw in part 1 of this series, we could build a set of overloads with our bare hands, or use variadic templates with a SFINAE constraints that would enable_if the parameters are strings. And we discussed the advantages and drawbacks of those solutions.

Now let’s we see two more alternatives: using a static_assert, and using template explicit instantiation, and we also compare their advantages and drawbacks. So when you encounter the case you can pick a solution with a trade-off that suits you best.

And if you think of another approach, you’re more that welcome to share it!

Here are the contents of the series:

Part 1 – The previous 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 – This article:

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

Solution 2: Being static_assertive

The approach using SFINAE consisted in creating an expression that indicates whether or not the types in a variadic pack are all std::string:

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

This expression is compatible with C++11, provided that we write the conjunction function ourselves (which we did in Part 1). It may be hard to read if you’re not used at SFINAE, and you can see how we got to it in Part1. But this expression didn’t show directly in the interface anyway.

Given a variadic pack Ts... we can derive a boolean that indicates whether all of the Ts are strings:

AllStrings<Ts...>::value

And we had fitted this expression inside of an enable_if.

But why not use this expression inside of a static_assert?

template<typename... Ts>
void f(Ts const&... ts)
{
    static_assert(AllStrings<Ts...>, "All parameters must be std::string");
    
    // ...
}

This way, if someone calls f by passing something else than a std::string, the code won’t compile. And the compilation error will contain the message we associated to the static assert: "All parameters must be std::string". That’s convenient.

Let’s compare this with the solution using enable_if (and this is the prettiest version using C++14’s enable_if_t):

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

The static_assert expression is arguably clearer than the enable_if expression, at least for two reasons:

  • the static_assert features a message written by humans for humans, in the string "All parameters must be std::string",
  • the syntax of the static_assert is less convoluted than the enable_if which transfigures the return type with template mechanics.

However the static_assert has the drawback of not being part of the function’s prototype. To use the static_assert as a mean of documentation, one has to look at the implementation of the function. It’s at the very beginning of it though. But still, it’s not as exposed as the function’s prototype.

Similarly to the enable_if, static_assert only authorizes std::string. In particular it won’t let in types that are convertible to std::string such as string literals of type const char*.

Advantages of the static_assert:

  • unlimited number of parameters,
  • readable expression,
  • explicit compilation error message when the constraint is not respected.

Drawbacks of the static_assert:

  • not in the function’s prototype,
  • no possible conversion (from string literals for example),
  • the implementation of the function template has to be in the header file.

Solution 3: a little-known feature: explicit template instantiation

Since they use template functions, the solutions using static_assert or enable_if force us to put the implementation of the body of f inside a header file if f is to be used in another .cpp file.

Indeed, templates don’t generate assembly code in themselves. It is only when they are instantiated in a certain context, like a call to the function f from another .cpp file for example, that the compiler actually generates code corresponding to f, with the types passed by the context. So the compiler compiling the file that calls f has to know how to instantiate f and needs to see its body for that.

This creates a problem of encapsulation, and of compile time dependencies: every time we change the implementation of f, all the files that include its header will have to recompile.

This is not the case for a regular, non-template function. If we change the implementation of a regular function in its own .cpp file, the other .cpp files that call it won’t notice a thing and won’t need to recompile since they only see a header file (that contains only the declaration of f), which is not modified.

But this constraint of putting the implementation of a template in a header file is valid only when we can’t know in advance what types the call site will use to instantiate f. For example, the class std::vector has all its code in a header, since it could be instantiated with any type on the planet.

In our case, the situation is different: we want our function to be instantiated only with std::strings.

And when you know what types to use with a template, you can instantiate this template manually in a .cpp file. The compiler will generate code for those particular instantiations and they will be considered just like any other functions of a .cpp file. In particular, we won’t need their code to be visible in the header.

This is called explicit template instantiation. We already used it in the “Extract Interface” refactoring, at compile time.

What does it look like?

In the header file, we only put the declaration of f:

template<typename... Ts>
void f(Ts const&... xs);

Note that we don’t put the body of f in the header. Instead, we put it in a .cpp file:

// in a .cpp file:

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

And we create the instantiations we want for f in this same .cpp file, with the following syntax:

template void f(std::string const&);

This generates the template code of f for a variadic pack Ts equal to one std::string.

Unfortunately, there isn’t (to my knowledge) a way to perform explicit template instantiation on variadic templates (if you know one, please shout!!).

So the best we can do here is to set an arbitrary limit, say 7, and generate the overloads manually in the .cpp file:

template void f(std::string const&);
template void f(std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);

But, contrary to the very first solution we saw where we wrote out the overloads manually, we don’t have to implement those overloads here. A mere declaration is enough to have the compiler instantiate the body of f with those types, and make them available to the linker.

Here is a summary of the code put together:

In the calling code, say main.cpp:

#include <f.hpp>
#include <string>

int main()
{
   f(std::string("X"), std::string("Y"));
}

In the header file, say f.hpp, we have:

template<typename... Ts>
void f(Ts const&... ts);

And in the .cpp with the implementation of f, say f.cpp, we have:

#include <f.hpp>
#include <string>

template<typename... Ts>
void f(Ts const&... ts)
{
    // body of f...
}

template void f(std::string const&);
template void f(std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);

Now if we change the code in main and replace it with:

int main()
{
   f(std::string("X"), 42);
}

We get the folioing error message by the linker:

main.obj : error LNK2019: unresolved external symbol "void __cdecl f<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,int>(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const &,int const &)" (??$f@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@H@@YAXABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@ABH@Z) referenced in function _main

Which means in essence: “couldn’t find an overload of f that takes a std::string and an int“. Indeed, we didn’t define it in the .cpp file, which is the point of the technique.

Note that, contrary to the other solutions, the interface of f in the header file doesn’t show anything about that constraint. That’s a problem. To remedy to this, we can try and include some indication about this by using naming and comments:

// f accepts only std::string arguments
template<typename... Strings>
void f(Strings const&... inputs);

Even though those messages are only made by humans for humans.

So in summary:

Advantages of explicit template instantiation:

  • All the implementation in a .cpp file,
  • no complicated syntax in the interface,

Drawback of explicit template instantiation:

  • not visible in the function’s prototype (unless we use naming or comments),
  • no possible conversion (from string literals for example),
  • relies on a little-known feature, which can be surprising for someone who is not familiar with it.

So, how should I define a variadic number of arguments of the same type?

Let’s recap all the advantages and drawbacks of the methods that we analyzed. If you see an aspect of them that I missed, or if you think of another technique, by all means let me know!

I hope this will help you choose the right trade off for your code.

Building the overloads with your own hands

Code:

// In a .cpp file:

void f(std::string const& input)
{
    // body of f...
}

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);
}

//////////////////////////////////////////////////////
// In a header file:

void f(std::string const& input);
void f(std::string const& input1, std::string const& input2);
void f(std::string const& input1, std::string const& input2, std::string const& 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);

Advantages:

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

Drawbacks:

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

A pinch of SFINAE

Code:

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

Advantages:

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

Drawbacks:

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

static_assert

Code:

template<typename... Ts>
void f(Ts const&... ts)
{
    static_assert(AllStrings<Ts...>, "All parameters must be std::string");
    
    // body of f...
}

Advantages:

  • unlimited number of parameters,
  • readable expression,
  • explicit compilation error message when the constraint is not respected

Drawbacks:

  • not in the function’s prototype,
  • no possible conversion (from string literals for example),
  • the implementation of the function template has to be in the header file.

Explicit template instantiation

Code:

// main.cpp, the calling code:

#include <f.hpp>
#include <string>

int main()
{
   f(std::string("X"), std::string("Y"));
}

/////////////////////////////////////////////////
// f.hpp, the header file:

template<typename... Ts>
void f(Ts const&... ts);

/////////////////////////////////////////////////
// f.cpp:

#include <f.hpp>
#include <string>

template<typename... Ts>
void f(Ts const&... ts)
{
    // body of f...
}

template void f(std::string const&);
template void f(std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);
template void f(std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&, std::string const&);

Advantages:

  • all the implementation in a .cpp file,
  • no complicated syntax in the interface,

Drawback:

  • doesn’t allow any number of parameter, there is an arbitrary limit,
  • not visible in the function’s prototype (unless we use naming or comments),
  • no possible conversion (from string literals for example),
  • relies on a little-known feature, which can be surprising for someone who is not familiar with it.

Your reactions are, as usual, welcome.

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