Home > Software engineering >  Why must C function parameter packs be placeholders or pack expansions?
Why must C function parameter packs be placeholders or pack expansions?

Time:05-30

The declarator for a C 20 function parameter pack must either be a placeholder or a pack expansion. For example:

// OK, template parameter pack only, no function parameter pack
template<unsigned ...I> void good1() {}

// OK function parameter pack is pack expansion of decltype(I)
template<unsigned ...I> void good2(decltype(I)...i) {}

// OK contains placeholder auto
void good3(std::same_as<unsigned> auto...i) {}

// OK contains placeholder auto
void good4(std::convertible_to<unsigned> auto...i) {}

// Error, no pack expansion or placeholder
template<unsigned = 0> void bad(unsigned...i) {}

This seems to make it impossible to declare a function that takes a variable number of parameters of a specific type. Of course, good2 above will do it, but you have to specify some number of dummy template arguments, as in good2<0,0,0>(1,2,3). good3 sort of does it, except if you call good3(1,2,3) it will fail and you have to write good3(1U,2U,3U). I'd like a function that works when you say good(1, 2U, '\003')--basically as if you had an infinite number of overloaded functions good(), good(unsigned), good(unsigned, unsigned), etc.

good4 will work, except now the arguments aren't actually of type unsigned, which could be a problem depending on context. Specifically, it could lead to extra std::string copies in a function like this:

void do_strings(std::convertible_to<std::string_view> auto...s) {}

My questions are:

  1. Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type? (I guess the one exception is C strings, because you can make the length a parameter pack as in template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/}, but I want to do this for a type like std::size_t)

  2. Why does the standard impose this restriction?

update

康桓瑋 asked why not use good4 in conjunction with forwarding references to avoid extra copies. I agree that good4 is the closest to what I want to do, but there are some annoyances with the fact that the parameters are different types, and some places where references do not work, either. For example, say you write code like this:

void
good4(std::convertible_to<unsigned> auto&&...i)
{
  for (auto n : {i...})
    std::cout << n << " ";
  std::cout << std::endl;
}

You test it with good(1, 2, 3) and it seems to work. Then later someone uses your code and writes good(1, 2, sizeof(X)) and it fails with a confusing compiler error message. Of course, the answer was to write for (auto n : {unsigned(i)...}), which in this case is fine, but there might be other cases where you use the pack multiple times and the conversion operator is non-trivial and you only want to invoke it once.

Another annoying problem arises if your type has a constexpr conversion function that doesn't touch this, because in that case the function won't work on a forwarding reference. Admittedly this is highly contrived, but imagine the following program that prints "11":

template<std::size_t N> std::integral_constant<std::size_t, N> cnst = {};

constexpr std::tuple tpl ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');

inline const char *
stringify(std::convertible_to<decltype(cnst<1>)> auto...i)
{
  static constexpr const char str[] = { get<i>(tpl)..., '\0' };
  return str;
}

int
main()
{
  std::cout << stringify(cnst<1>, cnst<1>) << std::endl;
}

If you change the argument to stringify to a forwarding reference stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i), it will fail to compile because of this.

CodePudding user response:

Why does the standard impose this restriction?

Most likely because this would confuse(or create complications) users with the old C varargs function that have almost same syntax(with unnamed parameter) as shown below:

//this is a C varargs function
void good3(int...) 
           ^^^^^^
{
    
}

Now if the fourth function bad was allowed:

 
template<int = 0> void bad(int...i)
                           ^^^^^^^ --->this is very likely to confuse users as some may consider it as a C varargs function instead of a function parameter pack 
{
}

IMO the above seems atleast a little ambiguous due to the similarity in the syntax of C varargs and a function parameter pack.

From dcl.fct#22:

There is a syntactic ambiguity when an ellipsis occurs at the end of a parameter-declaration-clause without a preceding comma. In this case, the ellipsis is parsed as part of the abstract-declarator if the type of the parameter either names a template parameter pack that has not been expanded or contains auto; otherwise, it is parsed as part of the parameter-declaration-clause.


I'd like a function that works when you say good(1, 2U, '\003')

You can use std::common_type for this.

template <typename... T, typename l= std::common_type_t<T...>>
void func(T&&... args) {
}

Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type?

The good3 given in your example is very readable and should be used from C 20. Though if one is using C 17 then one way to do this is using the SFINAE principle with a combination of std::conjunction and std::is_same as shown below.

Method 1

Here we simply check if all the arguments passed are of the same type.

template <typename T, typename ... Args>
std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
func(const T & first, const Args & ... args)
{
    std::cout<<"func called"<<std::endl;
    return true;
}
int main()
{
    func(4,5,8);    //works
    //func(4,7.7); //wont work as types are different
    std::string s1 = "a", s2 = "b"; 
    func(s1, s2);  //works
    //func(s1, 2);   //won't work as types are different
    
}

Working demo


Looking at your comment it seems you want to add one more restriction that the program should work only when

a) all the arguments are of the same type

b) all of those matches a specific type say int, or std::string.

This can be done by adding a static_assert inside the function template:

static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");

Method 2

Here use the static_assert shown above to check if the arguments passes are of a specific type like int.

template <typename T, typename ... Args>
std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
func(const T & first, const Args & ... args)
{
    static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");
    std::cout<<"func called"<<std::endl;
    return true;
}

int main()
{
    func(3,3);    //works
    //func(4,7.7); //wont work as types are different
    std::string s1 = "a", s2 = "b"; 
    //func(s1, s2);  //won't work as even though they are of the same type but not int type
    
}

Working demo


CodePudding user response:

Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type?

If I understand the question correctly, you can work around it by adding a proxy function that casts and forwards arguments to actual implementation:

unsigned wrap_sum(auto&&...args) {
  return []<std::size_t...I>(std::index_sequence<I...>,
                             std::conditional_t<true, unsigned, decltype(I)> ... args) {

    return (0u   ...   args);

  }(std::make_index_sequence<sizeof...(args)>{},
    std::forward<decltype(args)>(args)...);
}
  • Related