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?
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.
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