How to pass a variadic number of arguments of the same type with C++20

Example #1: passing a variadic number of arguments of exactly the same type

Let’s say we have a function that accepts a vector of strings and a variadic number of strings that will be added to the vector. If we want to check at compile-time that the arguments are all of std::string type, we can do the following:

#include <concepts>
#include <vector>
#include <string>

void add(std::vector<std::string>& v, std::same_as<std::string> auto const&... args) {
(v.push_back(args), ...);
}

int main() {
std::vector<std::string> v;

std::string s1{"hello"};
std::string s2{"concepts"};

add(v, s1, s2);

// ...
}
std::vector<std::string> v;

std::string s1{"hello"};
add(v, s1, std::string{"concepts"});

Example #2: perfect-forwarding a variadic number of arguments of the same type

To have perfect forwarding, we have to change the definition of our “add” function. Unfortunately, things get a little bit tricky…

void add(std::vector<std::string>& v, std::same_as<std::string> auto&&... args) {
(v.push_back(std::forward<decltype(args)>(args)), ...);
}
add(v, std::string{"hello"}, std::string{"concepts"});
// attempt to support constrained perfect forwarding: DOES NOT WORK
template <std::same_as<std::string>... Args>
void add(std::vector<std::string>& v, Args&&... args) {
(v.push_back(std::forward<Args>(args)), ...);
}

// alternate syntax: DOES NOT WORK EITHER
template <typename... Args>
requires (std::same_as<std::string, Args> && ...)
void add(std::vector<std::string>& v, Args&&... args) {
(v.push_back(std::forward<Args>(args)), ...);
}
// Perfect forwarding arguments with concepts: a working solution
template <typename... Args>
requires (std::same_as<std::string, std::remove_cvref_t<Args>> && ...)
void add(std::vector<std::string>& v, Args&&... args) {
(v.push_back(std::forward<Args>(args)), ...);
}
std::vector<std::string> v;

std::string s1{"hello"};
std::string s2{"concepts"};

add(v, s1, s2); // all l-values
add(v, std::string{"hello"}, std::string{"concepts"}); // all r-values
add(v, s1, std::move(s2), std::string{"!!!"}); // mixed l-values/r-values

Example #3: passing a variadic number of arguments convertible to a certain type

Sometimes, we would like not to restrict our arguments to a single type, but to several types as long as they are convertible to the one we are interested in. For example, in our case it makes sense to allow passing C-style strings to the function without having to explicitly instantiate a std::string, like this:

add(v, "hello", "concepts", "!!!");
add(v, s1, std::move(s2), "!!!"); // mixed l-values/r-values and C-style strings
void add(std::vector<std::string>& v, std::convertible_to<std::string> auto&&... args) {
(v.push_back(std::forward<decltype(args)>(args)), ...);
}
template <std::convertible_to<std::string>... Args>
void add(std::vector<std::string>& v, Args&&... args) {
(v.push_back(std::forward<Args>(args)), ...);
}

// OR (it's really the same)

template <typename... Args>
requires (std::convertible_to<Args, std::string> && ...)
void add(std::vector<std::string>& v, Args&&... args) {
(v.push_back(std::forward<Args>(args)), ...);
}

Conclusions

While it would be good to have homogeneous variadic parameters directly supported by the language, by using Concepts we have a working solution relatively clean and readable.

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store