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.
Update: something old and something new
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.