简体   繁体   中英

Overload variadic template's fixed parameters

Is it possible to override a variadic template by changing the number of fixed parameters before the function parameter pack? For example:

#include <iostream>

template <typename ...Args>
void foo(std::string, std::string, std::string, Args...) 
{ 
  std::cout << "THREE STRINGS\n"; 
}

template <typename ...Args>
void foo(std::string, std::string, Args...) 
{ 
  std::cout << "TWO STRINGS\n"; 
}

int main() {
  foo("key", "msg", "data", 1);
}

Running this causes the second foo to be called, but I want the first to be called. Is there a better way to overload this function?

Second variant is selected because it does not involve an extra conversion required to create an instance of std::string for the last argument. If you call class constructors explicitly (or adjust arguments to take exactly what you pass) then it will work fine:

foo(std::string{"key"}, std::string{"msg"}, std::string{"data"}, 1, 2); // THREE STRINGS

online compiler

This is because a string literal is not of type std::string . It is of type const char[N] . So the type of the third parameter of the function instantiated from the second function template is deduced to be const char* , which is considered a better match than the function instantiated from the first function template.

You can change the type of the parameters from std::string to const char* , or use explicit conversions suggested by VTT's answer.

As explained by VTT and xskxzr, "data" is a string literal, so is a const char [5] , so is convertible to but not exactly a std::string , so a generic template type is a better match than a std::string and the compiler prefer the first version.

You can choose the first version of foo() passing a std::string{"data"} but, if you don't want to change the call, another possible solution is SFINAE enable/disable the second version of foo() .

I mean... if you write a fIsCToS (as "first is convertible to string") custom type traits as follows

template <typename...> // for empty `Args...` list case
struct fIsCToS : std::false_type
 { };

template <typename T0, typename ... Ts>
struct fIsCToS<T0, Ts...> : std::is_convertible<T0, std::string>
 { };

you can rewrite the second version of foo() using it as follows

template <typename ...Args>
typename std::enable_if<false == fIsCToS<Args...>{}>::type
   foo(std::string, std::string, Args...) 
 { std::cout << "TWO STRINGS\n"; }

The following is your modified example

#include <iostream>

template <typename...>
struct fIsCToS : std::false_type
 { };

template <typename T0, typename ... Ts>
struct fIsCToS<T0, Ts...> : std::is_convertible<T0, std::string>
 { };

template <typename ...Args>
void foo(std::string, std::string, std::string, Args...) 
 { std::cout << "THREE STRINGS\n"; }

template <typename ...Args>
typename std::enable_if<false == fIsCToS<Args...>{}>::type
   foo(std::string, std::string, Args...) 
 { std::cout << "TWO STRINGS\n"; }

int main ()
 {
   foo("key", "msg", "data", 1, 2); // now print THREE STRINGS
 }

If you can use C++14, you can use std::enable_if_t instead of typename std::enable_it<...>::type so the second foo() can be simplified as

template <typename ...Args>
std::enable_if_t<false == fIsCToS<Args...>{}>
   foo(std::string, std::string, Args...) 
 { std::cout << "TWO STRINGS\n"; }

OK, so I came up with a different solution. I hate answering my own question, but this is sort of the answer I was looking for and maybe it can help others.

What I did instead was to not overload the templated function and instead combine the entire signature into a single parameter pack. Then, I overloaded a non-templated function that does the actual work, using std::forward to forward the args to one of these overloaded functions. In this way, I let the compiler decide which method to call.

My solution:

#include <iostream>

void print_call(std::string key, std::string msg, std::string data, int)
{
  std::cout << "THREE STRINGS\n"; 
}

void print_call(std::string key, std::string msg, int)
{
  std::cout << "TWO STRINGS\n"; 
}

template <typename ...Args>
void foo(Args &&...args) 
{ 
  print_call(std::forward<Args>(args)...);
}

int main() {
  foo("key", "msg", 1);

  foo("key", "msg", "data", 1);
}

See it running here :

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM