bpeikes
bpeikes

Reputation: 3705

Using SFINAE to check function std::to_string exists for a type

I want to create a function template that is specialized for types that may have std::to_string applied to them, another for classes for which I have defined a mylib::to_string, and let any types for which it may not fall through to other implementations or specializations.

Example:

// I only want this to be found if std::to_string exists for ItemType
template<typename ItemType>
void func(ItemType &i)
{
     std::cout << std::to_string(i);
}

// I only want this to be found if mylib::to_string exists for ItemType
template<typename ItemType>
void func(ItemType &i)
{
     std::cout << mylib::to_string(i);
}

// And finally, I'd like to be able to fall through to anything that has a << defined for streams
template<>
void func<MyClass>(MyClass &i)
{
     std::cout << MySpecialConverterFunc(i);
}

I always have problems with std::enable_if syntax. How would you add it to the first template?

Upvotes: 4

Views: 951

Answers (2)

Alecto
Alecto

Reputation: 10750

Avoid SFINAE and write simple code using the ADL rule

You can avoid having to use SFINAE by taking advantage of ADL (Argument Dependent Lookup). What ADL states is that:

If a function and a type are both declared in the same namespace, you don't have to specify the namespace when invoking that function.

For example:

namespace foo 
{
    class MyClass {};
    std::string to_string(MyClass const& x) {
        return "[Element of MyClass]"; 
    } 
}

int main() {
    foo::MyClass x;

    std::cout << to_string(x); // This is legal because x is in namespace foo
}

If you write to_string functions in your namespace for your types, then you can write func once in terms of that:

template<class T>
void func(T const& t) {
    using std::to_string; 
    // This calls std::to_string if std::to_string is defined;
    // Otherwise it uses ADL to find the correct to_string function
    std::cout << to_string(t); 
}

In this example, func can be called with foo::MyClass because there's a to_string function that accepts MyClass in namespace foo:

int main() {
    func(10); // Prints 10; 
    func(3.1415); // Prints 3.1415

    foo::MyClass x;
    func(x); // Prints [Element of MyClass]
}

What about types in namespaces I don't own?

In this case, I'd recommend creating an additional namespace and sticking the to_string functions there, and then adding that as a using statement:

namespace textConvert {
    // Because we use std::to_string here, it automatically gets included
    // When we use textConvert::to_string
    using std::to_string; 

    // Convert a vector
    template<class T>
    std::string to_string(std::vector<T> const& vect) {
        using std::to_string; 

        std::string str = "[";        
        for(auto& elem : vect) {
            str += to_string(elem);
            str += ','; 
        }
        str.back() = ']';
        return str; 
    }
}

Then, we can update func to include textConvert::to_string, and because textConvert uses std::to_string in addition to the various custom conversion functions, both get included!

template<class T>
void func(T const& t) {
    // This *also* automatically includes std::to_string
    // Because we put using std::to_string inside textConvert
    using textConvert::to_string;

    std::cout << to_string(t);
}

Upvotes: 2

For SFINAE you need not to specialize, but to overload. Which is saner than dealing with specializations anyway (those have quirks). SFINAE is a mechanism to guide overload resolution, not for choosing explicit specializations of function templates.

As for the check itself, you don't need enable_if really. Just a little bit of expression SFINAE will do:

template<typename ItemType>
auto func(ItemType &i) -> decltype(std::to_string(i), void())
{
     std::cout << std::to_string(i);
}

void func(MyClass &i)
{
     std::cout << MySpecialConverterFunc(i);
}

In the above code, decltype is applied to a comma expression. The left hand side is std::to_string(i), if it isn't well-formed, the whole expression isn't well formed, and we get a substitution error, making the compiler discard that overload (the "not an error" part). If it's well-formed, the right hand side is a void() literal, so decltype resolves to void as you intended.

When substitution fails, all we are left with is the second overload, so it's picked. It would have been picked for this case anyway, because non-template overloads are always favored in overload resolution if the signature is the same. But adding a check to better guide the compiler in other cases is not a bad idea in and of itself.

Upvotes: 5

Related Questions