Search code examples
c++templatessfinaecompile-time

How to constrain a template? Six different usages of std::enable_if


I'm trying to understand two different versions of a template function that uses std::enable_if<>.

Version 1:

template<class T, typename std::enable_if<std::is_convertible<T, std::string_view>::value, T>::type* = nullptr>
void foo(const T& msg);

Version 2:

template<class T, typename = typename std::enable_if<std::is_convertible<T, std::string_view>::value>::type>
void foo(const T& msg);

If I understood it correctly, if the condition is met they should be converted into:

// Version 1
template<class T, T* = nullptr>
void foo(const T& msg);

// Version 2
template<class T, typename = void>
void foo(const T& msg);

Both versions can be equally called by:

std::string s = "Test";
foo(s);

What is the difference between those two versions? When should one be used?


Second question

Because of an error on my part, I discovered that version 2 also compiles, if one typename is missing:

//Correct Version 2 like above:
template<class T, typename = typename std::enable_if<std::is_convertible<T, std::string_view>::value>::type>
void foo(const T& msg);

// My "faulty" version, also works. Is this correct too?
template<class T, typename = std::enable_if<std::is_convertible<T, std::string_view>::value>::type>
void foo(const T& msg);

Is the second (faulty) version also correct? I thought std::enable_if<> does need a typename in front of it.


Solution

  • How should one constrain a template?

    If you are not limited to compatibility with older C++ standards (pre C++20), and you don't need to refer to the template type, and the constraints only involve a single template parameter, prefer the least boilerplate option:

    // #1
    void foo(const std::convertible_to<std::string_view> auto& msg);
    

    Otherwise, prefer the slightly more verbose form:

    // #2
    template <typename T>
        requires std::convertible_to<T, std::string_view>
    void foo(const T& msg);
    

    The form #2 gives a name to the template type and continues to function if the constraints involve multiple template parameters. It is still not directly applicable to older C++, but the location of the constraint is compatible with older C++ enable_if usage:

    // #2, compatible version
    
    // C++11
    #define TEMPLATE(...)            template <__VA_ARGS__
    #define REQUIRES(C)              , typename std::enable_if<(C), int>::type = 0>
    #define CONVERTIBLE_TO(From, To) std::is_convertible<From, To>::value
    
    // C++20
    #define TEMPLATE(...)            template <__VA_ARGS__>
    #define REQUIRES(C)              requires (C)
    #define CONVERTIBLE_TO(From, To) std::convertible_to<From, To>
    
    TEMPLATE(typename T)
        REQUIRES(CONVERTIBLE_TO(T, std::string_view))
    void foo(const T& msg);
    

    The following options are also available, but I would stick to #1 or #2:

    // #3
    template <std::convertible_to<std::string_view> T>
    void foo(const T& msg);
    
    // #4
    template <typename T>
    void foo(const T& msg) requires std::convertible_to<T, std::string_view>;
    

    With respect to enable_if, there are three options:

    // #5, non-type template parameter with default value ("version 1")
    template <typename T, typename std::enable_if_t<std::is_convertible_v<T, std::string_view>, int> = 0>
    void foo(const T& msg);
    
    // #6, enable_if in the return type
    template<typename T>
    auto foo(const T& msg) -> typename std::enable_if_t<std::is_convertible_v<T, std::string_view>>;
    
    // #7, defaulted template parameter ("version 2")
    template<class T, typename = typename std::enable_if_t<std::is_convertible_v<T, std::string_view>>>
    void foo(const T& msg);
    

    Option #7 ("version 2") is rarely advisable, because default template parameters do not participate in the function signature. So, once you have two overloads, it is ambiguous. And overload sets grow.

    Option #6 is not available for constructors, which lack a return type. But, in #6, you can name the function parameters which can be handy.

    Option #5 is the most general SFINAE option. Prefer it, if you must SFINAE.

    Regarding question #2, the relaxation on typename came in C++20, and is described here and here