Reputation: 778
I want to have a function that takes a std::string
and another that takes anything that is not a std::string
.
So I have:
#include <iostream>
#include <string>
void foo(const std::string& str) {
std::cout << "string version called" << std::endl;
}
template<class T>
void foo(T&& str) {
std::cout << "template version called" << std::endl;
}
int main() {
foo(std::string{"hello"});
return 0;
}
The problem is that the templated version is called instead of the std::string
version.
So how can I have a foo
function that either takes anything or specifically a std::string
?
Upvotes: 0
Views: 880
Reputation: 5642
Use an explicit specialization rather than an overloaded function.
template<>
void foo<std::string>(const std::string& str) {
...
I think that's right; explicit specializations of free functions was not original to C++98 but added later at some point.
(it's still a pain to get right, as your specialization has to match the actual type that was deduced for T
, which may include const
and &
if those were not present in the argument list; I've used it effectively for plain pass-by-value types like when the special type is int
)
In C++20, you could use a requires
clause to make the template not apply for std::string
. If requires
is not available, you can do the same thing using enable_if
.
You might also just write one function, but use constexpr if
in the body to provide the special case. This prevents the overloading mechanism from getting involved at all, and lets you code the exact rules for determining the special case.
In the original template specification, function templates could not be explicitly specialized and the idea was that you overload functions instead. You see why this doesn't work as intended: the template is always an exact match, even when the specific function would be called (using trivial conversions, adding const
, passing by reference) if overloading just non-template functions.
The work-around was to make a dummy class template, holding a static member function. So, if you originally had a function template f
and you needed to explicitly specialize it, move the function body into:
template <typename T>
struct C {
void f (const T&) { /* body goes here */ }
};
Now you can write an explicit specialization of C
, and thus C::f
:
template<>
struct C<std::string> {
static void f (const std::string&) { /* special code goes here */ }
};
and then, to retain compatibility with the existing code, write a new body for the (non-member) f
that just calls C<T>::f
.
Now, doing that today you would make it even better and use perfect forwarding.
template <typename T>
void f (const T&& param)
{
C<T>::f(std::forward<T>(param));
}
Now, look at how this differs from just being able to explicitly specialize a function. The template argument deduction is done on the wrapper call, and then the determined value of T
is used for the class template instantiation, and then the final member function call does not do any deduction but rather will apply conversions as for normal function calls. It doesn't have the "always a perfect match" behavior. The exact form of the argument can vary; e.g. whether you are passing a value or a const
reference.
In fact, thanks to perfect forwarding, it preserves the value category of the wrapper's call, and you can actually overload f
within one of the explicit specializations! That is, you could have a separate form for rvalues, constants or non-const, etc.
Here, the wrapper function was declared with const
which loses the ability to distinguish non-const parameters, but this makes it easy to have template argument deduction not include the const
. You could add your own normalization step to transform the actual argument's type into the plain T
you wanted, instead. In fact, you can add any metaprogramming logic you want, such as recognising base and derived classes, which is another issue that shows up with overloading and templates.
Upvotes: 1
Reputation: 2564
The problem with your code is that you are creating a temporary variable std::string{"Hello"}
which is an exact match for T&&
(better than std::string const&
) and therefore the templated version is chosen. You could either
Specialise the template for std::string
template<typename T>
void foo(T t) {
// Your implementation for other data types
}
// Template specialisation for strings
template <>
void foo<std::string>(std::string str) {
// Your implementation for strings goes here
}
Overload the function and de-activate the template for std::string
, std::string const&
, std::string&&
, ...
For the latter you can use the std::decay<T>
type trait struct to disable all of these versions at once in combination with std::is_same
.
// Function overload for strings
void foo(std::string const& str) {
// Your implementation for strings goes here
}
// Template disabled for strings
template<class T>
typename std::enable_if<!std::is_same<std::decay<T>::type, std::string>::value>::type
foo(T&& t) {
// Your implementation for the other data types
}
You could compare the typeid
or even better in C++17 there is a constexpr if
available which could be combined with std::is_same<T>
template<class T>
void foo(T&& t) {
if constexpr (std::is_same_v<T, std::decay_t<std::string>>) {
// Your implementation for strings
} else {
// Your implementation for the other data types
}
}
Furthermore if you wanted to make something like
foo("Hello");
work as well (so instead of foo(std::string{"Hello"})
) you could take it a step further also excluding any sort of char*
// Gets used for std::string as well as char*
void foo(std::string const& str) {
std::cout << "string version called" << std::endl;
}
// Disable template for any std::string and char*
template<class T>
typename std::enable_if<!std::is_same<typename std::decay<T>::type, std::string>::value &&
!std::is_same<typename std::decay<T>::type, char const *>::value &&
!std::is_same<typename std::decay<T>::type, char *>::value>::type
foo(T&& t) {
std::cout << "template version called" << std::endl;
}
In this case also foo("Hello")
will call the overloaded std::string
version. Without this any call without std::string{}
will go to the templated version! Try it hereC++11 C++17.
Upvotes: 1
Reputation: 172864
The templated foo
is using forwarding reference, when being passed temporary std::string
like std::string{"hello"}
, after the deduction the function parameter str
would be std::string&&
, it's a better match than the non-template foo
which taking const std::string&
.
You can impose restrictions on the template parameter to make it usable only when being passed non-std::string
s. E.g.
template<class T>
std::enable_if_t<!std::is_same_v<std::decay_t<T>, std::string>>
foo(T&& str) {
std::cout << "template version called" << std::endl;
}
Upvotes: 3
Reputation: 275210
Your problem is you are taking by forwarding reference. An rvalue binds to std::string&&
better than std::string const&
, so yourtemplate ks preferred.
Change it to
template<class T>
void foo(T const& str) {
std::cout << "template version called" << std::endl;
}
Upvotes: 2
Reputation: 11
use typeid()
template <class T>
void foo(T &&str)
{
if (typeid(str) == typeid(string))
{
std::cout << "string version called" << std::endl;
}else{
std::cout << "template version called" << std::endl;
}
}
int main() {
foo(std::string{"hello"});
foo("hello");
return 0;
}
Upvotes: 1