Use Implicit conversion before Templating

Viewed 72

I'm trying to use a combination of template specialization and implicit conversion to sort templated types in to 3 categories:

  1. types that can be implicitly converted to a string
  2. types that can be implicitly converted to a double
  3. types that cannot be implicitly converted to a string nor double

In this attempt of mine, however, all types that could have been converted into either a double or a string end up in the default case of type T.

template<>
void Foo(std::string s)
{
    //can be converted to a string
}

template<>
void Foo(double d)
{
    //can be converted to a double
}

template<typename T>
void Foo(T t)
{
    //cannot be converted to a string or a double 
}

struct Other {};

int main()
{
    Foo("bar");// creates Foo(const char*) instead of converting to string
    Foo(1);// creates Foo(int) instead of converting to double
    Other o
    Foo(o);// creates Foo(Other)
}

On the other hand, implicit conversion works if the templating is removed, but at the cost of being able to handle the "neither" types.

void Foo(std::string s)
{
    //can be converted to a string
}

void Foo(double d)
{
    //can be converted to a double
}

struct Other {};

int main()
{
    Foo("bar");// const char* -> std::string
    Foo(1);// int -> double
    Other o
    Foo(o);// No overload available for this type
}

Is there a way to prioritize implicit conversion to existing specialized templates before creating a new templated function? Or perhaps is there an entirely different way I should be approaching this problem?

1 Answers

Don't specialize... Honestly, if there's a problem X with a solution Y, then specializing function templates is almost invariably Z. For a specialization to be called, the deduced template arguments must be exactly the ones we are specializing over. Moreover, whether a specialization exists or not isn't even considered in overload resolution! The overload is resolved from the primary template declaration alone.

For your problem you need to overload, so that the two custom cases are always considered in overload resolution. Then you just have to make sure the general catch-call function is simply discarded when needed. SFINAE, along with some standard type traits will accomplish exactly that.

#include <type_traits>

// Overloads. Not templates!

void Foo(std::string s)
{
    //can be converted to a string
}


void Foo(double d)
{
    //can be converted to a double
}

template<typename T>
std::enable_if_t<!(std::is_convertible_v<T, std::string> || std::is_convertible_v<T, double>)>
Foo(T t)
{
    //cannot be converted to a string or a double 
}

See how the condition is exactly what you want to check? The type traits verify if a conversion is possible, in which case std::enable_if_t is an ill-formed type (doesn't exist). So the overload is simply discarded (because Substitution Failure Is Not An Error). The other two overloads are still there, so one of them gets chosen.

If neither conversion is possible, then the template overload is not discarded, and std::enable_if_t - the return type of the function - is void (void is what enable_if_t resolves to by default).


If you have access to newer compilers that support the latest C++ standard, you can even write it in a way that is more user-friendly than classic SFINAE

#include <concepts>

template<typename T>
    requires !(std::convertible_to<T, std::string> || std::convertible_to<T, double>)
void Foo(T t)
{
    //cannot be converted to a string or a double 
}

The constraint can now occupy it's own place in the declaration, leaving the return type out of the magic.

Related