Std::Enable_If:Parameter VS Template Parameter

std::enable_if : parameter vs template parameter

Default template arguments are not part of the signature of a template (so both definitions try to define the same template twice). Their parameter types are part of the signature, however. So you can do

template <class T>
class check
{
public:
template< class U = T,
typename std::enable_if<std::is_same<U, int>::value, int>::type = 0>
inline static U readVal()
{
return BuffCheck.getInt();
}

template< class U = T,
typename std::enable_if<std::is_same<U, double>::value, int>::type = 0>
inline static U readVal()
{
return BuffCheck.getDouble();
}
};

enable_if, SFINAE and template parameters?

There are two kinds of template parameters:

  • type template parameters
  • non-type template parameters

The name of the template parameter is optional

A template parameter can have a default value

Type template parameter

named, non-defaulted

template <class T>
struct X {};

Usage:

X<int> x{};

named, defaulted

template <class T = int>
struct X {};

Usage:

X<> x1{};
X<char> x2{};

unnamed non-defaulted

template <class>
struct X {};

Usage:

X<int> x{};

unnamed, defaulted

template <class = int>
struct X {};

Usage:

X<> x1{};
X<char> x2{};

Non-type template parameter

named, non-defaulted

template <int I>
struct X {};

Usage:

X<24> x{};

named, defaulted

template <int I = 24>
struct X {};

Usage:

X<> x1{};
X<11> x2{};

unnamed, non-defaulted

template <int>
struct X {};

Usage:

X<11> x{};

unnamed, defaulted

template <int = 24>
struct X {};

Usage:

X<> x1{};
X<11> x2{};

For SFINAE the technique requires a default value. Since you don't care for the parameter inside the class you can skip its name. As for the type or non-type you can chose which one is easier to write in your particular case.

SFINAE using std::enable_if: type parameter vs non-type parameter

Because in the first case, a SFINAE failure remove only the default for a template parameter.

// in case F is true, you have 

template <bool F, typename = void>
int func1() { return 0; }

template <bool F, typename> // no more default but still available
int func1() { return 0; }

So doesn't disable the function, and you have two definitions of the same function (a default value doesn't change a function signature) so, as pointed by Jarod42 (thanks), you have a violation of the One Definition Rule.

In the second case you remove a template parameter, so you destroy the function, so no collision anymore.

template <bool F, int = 0>
int func2() { return 0; }

// template<bool F, ...> // function removed
// int func2() { return 0; }

You can verify that, in the first case, also the "disabled" function is still available, with a test with a single function

template <bool F, typename = std::enable_if_t<F>>
int foo ()
{ return 0; }

and calling it with true, false and false, void.

foo<true>();  // compile: foo<true, void>() called
foo<false>(); // compilation error: no second template paramenter
foo<false, void>(); // compile: explicit second template parameter

Six different usages of std::enable_if in conditionally compiled templates

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

Enable_if as template parameter

This happened:

  1. The author posted working code (<Policy> was present);

  2. There was some discussion in the comments that led the author to edit the code, and he made a mistake (<Policy> was removed);

  3. I rectified the mistake putting back the missing <Policy>.


Can anybody explains why even the edited version works?

When you attempt to instantiate Foo<T>, the declaration with the default template parameters is taken into account by the compiler. The default parameter is evaluated and if std::is_base_of<BasePolicy, Policy>::value is false then enable_if produces a SFINAE-friendly error.

If std::is_base_of<BasePolicy, Policy>::value is true, the partial specialization is chosen.

template <typename Policy>
struct Foo<Policy> {
Foo() { }
};

// is equivalent to

template <typename Policy>
struct Foo<Policy, void> {
Foo() { }
};

The above specializations are equivalent because typename std::enable_if<true>::type is void by default.

moving std::enable_if from parameters to template parameters

The problem is that the second template parameter in std::enable_if defaults to void. So your doing something which is pretty much the same as:

template <typename T, void = 0>

Which makes substitution fail always. You could use a non-type template argument of type int, which you can give a default 0 value:

template <typename T, typename std::enable_if< std::is_same<T, int>::value, int >::type = 0>

Do the same for both overloads, and it will work.

Demo here.

Why should I avoid std::enable_if in function signatures

Put the hack in the template parameters.

The enable_if on template parameter approach has at least two advantages over the others:

  • readability: the enable_if use and the return/argument types are not merged together into one messy chunk of typename disambiguators and nested type accesses; even though the clutter of the disambiguator and nested type can be mitigated with alias templates, that would still merge two unrelated things together. The enable_if use is related to the template parameters not to the return types. Having them in the template parameters means they are closer to what matters;

  • universal applicability: constructors don't have return types, and some operators cannot have extra arguments, so neither of the other two options can be applied everywhere. Putting enable_if in a template parameter works everywhere since you can only use SFINAE on templates anyway.

For me, the readability aspect is the big motivating factor in this choice.

Use a enable_if that does not depend on method's template parameter

Rather than try to SFINAE your way into two implementations, just use normal overload resolution.

#include <type_traits>
#include <iostream>

template<class T1, class T2>
class Foo {
template<class InnerT, class ... Args>
void do_bar(InnerT param, std::true_type, Args... args) { std::cout << "same" << std::endl; }

template<class InnerT, class ... Args>
void do_bar(InnerT param, std::false_type, Args... args) { std::cout << "not same" << std::endl; }

public:
template<class InnerT, class ... Args>
void bar(InnerT&& param, Args&&... args)
{
do_bar(std::forward<InnerT>(param), std::is_same<T1, T2>{}, std::forward<Args>(args)...);
}

};

int main() {
Foo<int, int> f1;
Foo<int, double> f2;

f1.bar(1, 2, 3);
f2.bar("Hello");
}

See it live

Default template argument when using std::enable_if as templ. param.: why OK with two template functions that differ only in the enable_if parameter?

Notes

A common mistake is to declare two function templates that differ only in their default template arguments. This is illegal because default template arguments are not part of function template's signature, and declaring two different function templates with the same signature is illegal.

Your functions don't differ only in their default template arguments, they differ in their template parameters, so have different signatures.

In both cases the default template argument is nullptr, but the second template parameter is different in each case.



Related Topics



Leave a reply



Submit