I have been working with templates for quite some time now but recently I encountered a weird template programming for SFINAE.
My question is why do we write
typename = {something} as a template parameter in C++?
That is simply how SFINAE works ;)
As you know, you have to "create a failure" within the template declaration and not inside the template definition. As this:
template < typename X, typename = ... here is the code which may generate an error during instantiation >
void Bla() {}
The only chance to put some "code" in the declaration is to define something in the template parameter list or inside the template function declaration itself like:
template < typename X>
void Bla( ... something which may generates an error ) {}
Example:
template <typename T, typename = std::enable_if_t< std::is_same_v< int, T>>>
void Bla()
{
std::cout << "with int" << std::endl;
}
template <typename T, typename = std::enable_if_t< !std::is_same_v< int, T>>>
void Bla(int=0)
{
std::cout << "with something else" << std::endl;
}
int main()
{
Bla<int>();
Bla<std::string>();
}
But what is the background of "creating an substitution failure" here?
The trick is somewhere behind std::enable_if. We can also use it without that:
template <typename T, typename = char[ std::is_same_v< int, T>]>
void Bla()
{
std::cout << "with int" << std::endl;
}
template <typename T, typename = char[ !std::is_same_v< int, T>]>
void Bla(int=0)
{
std::cout << "with something else" << std::endl;
}
Take a look on: typename = char[ !std::is_same_v< int, T>]
Here std::is_same_v gives us a bool value back which is casted to 0 if not valid and to any postive number if valid. And creating a char[0] is simply an error in c++! So with the negation of our expression with ! we got one for "is int" and one for "is not int". Once it tries to create an array of size 0, which is a failure and the template will not be instantiated and once it generates the type char[!0] which is a valid expression.
And as the last step:
... typename = ...
is meant as: there will be defined a type, here without a template parameter name, as the parameter itself is not used later. You also can write:
... typename X = ...
but as X is not used, leave it!
Summary: You have to provide some code, which generates an error or not, depending on the type or value of a given expression. The expression must be part of the function declaration or part of the template parameter list. It is not allowed to put an error inside the function/class definition, as this will not longer be "not an error" in the sense of SFINAE.
Update: Can the results of SFINAE expressions be used for further expressions: Yes
Example:
template < typename TYPE >
void CheckForType()
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
template <typename T, typename X = std::enable_if_t< std::is_same_v< int, T>, float>>
void Bla()
{
std::cout << "with int" << std::endl;
CheckForType<X>();
}
template <typename T, typename X = std::enable_if_t< !std::is_same_v< int, T>, double >>
void Bla(int=0)
{
std::cout << "with something else" << std::endl;
CheckForType<X>();
}
int main()
{
Bla<int>();
Bla<std::string>();
}
Output:
with int
void CheckForType() [with TYPE = float]
with something else
void CheckForType() [with TYPE = double]
Explanation:
std::enable_if has a second template parameter which is used as return type, if the first parameter of it will be true. As this, you can use that type here. As you can see, the function CheckForType is called with this defined type for X from the SFINAE expression.
An example :
//for enum types.
template <typename T, typename std::enable_if<std::is_enum_v<T>, int>::type = 0>
void Foo(T value)
{
//stuff..
}
I only want this Foo to be picked if T is an enum type. We can achieve this using the rule of Substitute Failure Is Not An Error (SFINAE).
This rule states that if there's a failure during substitution this shouldn't be a compilation error. The compiler should only eliminate the function for this substitution.
So then, how do we make sure Foo is only called with an enum type? Well that's easy, we find a way to trigger a substitution failure!
If we check cppreference for std::enable_if it says:
template< bool B, class T = void >
struct enable_if;
If B is true, std::enable_if has a public member typedef type, equal
to T; otherwise, there is no member typedef.
This means, if std::is_enum<T>::value is true (which is the case for enum types for T) then B will be true and thus type will be a valid type, an int as specified.
If however std::is_enum::value is false then B will be false and thus type won't even exist. In this case the code is trying to use ::type while it doesn't exist and thus that's a substitution error. So SFINAE kicks in and eliminates it from the candidate list.
The reason we use = 0 is to provide a default template parameter value since we're not actually interested in using this type or its value, we only use it to trigger SFINAE.
This construct is a nameless template parameter supplied with a default argument. Normally a template parameter would look like
typename <identifier> [ = <type> ]
but the identifier may be omitted.
You can see this construction often in SFINAE templates because it's a convenient way to enable a specialisation if and only if some condition holds, or if some piece of code is valid. Thus one can often see
template <typename T, typename = std::enable_if<(some-constexpr-involving-T)>::type> ...
If the expression happens to evaluate to true, all is well: std::enable_if<true>::type is just void, and we have a good working template specialisation.
Now if the expression above happens to be false, std::enable_if<false> doesn't have a member named type, Substitution Failure (the SF in SFINAE) happens, and the template specialisation is not considered.
And here the role of tge parameter ends. It is not used for anything in the template itself, so it doesn't need a name.
Related
Consider
#include <iostream>
#include <type_traits>
template <class T, class ARG_T = T&>
T foo(ARG_T v){
return std::is_reference<decltype(v)>::value;
}
int main() {
int a = 1;
std::cout << foo<int>(a) << '\n';
std::cout << foo<int, int&>(a) << '\n';
}
I'd expect the output to be 1 in both cases. But in the first case it's 0: consistent with the default being class ARG_T = T rather than class ARG_T = T&.
What am I missing?
For foo<int>(a), ARG_T is being deduced from a, and is not taken from the default template argument. Since it's a by value function parameter, and a is an expression of type int, it's deduced as int.
In general, default template arguments are not used when template argument deduction can discover what the argument is.
But we can force the use of the default argument by introducing a non-deduced context for the function parameter. For instance:
template <class T, class ARG_T = T&>
T foo(std::enable_if_t<true, ARG_T> v1){
//...
}
Or the C++20 type_identity utility, such as the other answer demonstrates.
You need to stop template argument deduction for ARG_T from the function argument v, (with the help of std::type_identity, which could be used to exclude specific arguments from deduction); Otherwise, the default template argument won't be used. e.g.
template <class T, class ARG_T = T&>
T foo(std::type_identity_t<ARG_T> v){
return std::is_reference<decltype(v)>::value;
}
LIVE
BTW: If your compiler doesn't support std::type_identity (since C++20), you might make your own.
template<typename T> struct type_identity { typedef T type; };
template< class T >
using type_identity_t = typename type_identity<T>::type;
So yet another question in this saga. Guillaume Racicot has been good enough to provide me with yet another workaround so this is the code I'll be basing this question off of:
struct vec
{
double x;
double y;
double z;
};
namespace details
{
template <typename T>
using subscript_function = double(*)(const T&);
template <typename T>
constexpr double X(const T& param) { return param.x; }
template <typename T>
constexpr double Y(const T& param) { return param.y; }
template <typename T>
constexpr double Z(const T& param) { return param.z; }
}
template <typename T, typename = void>
constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };
template <typename T>
constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>>[] = { &details::X<T>, &details::Y<T>, &details::Z<T> };
int main() {
vec foo = { 1.0, 2.0, 3.0 };
for(const auto i : my_temp<decltype(foo)>) {
cout << (*i)(foo) << endl;
}
}
The problem seems to arise in my specialization when I return something other than void. In the code above for example, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T> prevents specialization, while simply removing the last argument and allowing enable_if to return void allows specialization.
I think this points to my misunderstanding of what is really happening here. Why must the specialized type always be void for this to work?
Live Example
Not sure to understand what you don't understand but...
If you write
template <typename T, typename = void>
constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };
template <typename T>
constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>>[] = { &details::X<T>, &details::Y<T>, &details::Z<T> };
you have a first, main, template variable with two templates: a type and a type with a default (void).
The second template variable is enabled when std::enable_if_t is void.
What's happen when you write
for(const auto i : my_temp<decltype(foo)>)
?
The compiler:
1) find my_temp<decltype(foo)> that has a single template parameter
2) look for a matching my_temp template variable
3) find only a my_temp with two template parameters but the second has a default, so
4) decide that my_temp<decltype(foo)> can be only my_temp<decltype(foo), void> (or my_temp<vec, void>, if you prefer)
5) see that the main my_temp matches
6) see that the my_temp specialization doesn't matches because
enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>
is T (that is vec), so could match only my_temp<vec, vec> that is different from my_temp<vec, void>.
7) choose the only template variable available: the main one.
If you want that the specialization is enabled by
enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>
you should use T
// ..............................V T! not void
template <typename T, typename = T>
constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };
as default for second template type in the main template variable.
Off Topic suggestion: better use std::declval inside the std::is_floating_point_v test; I suggest
std::enable_if_t<std::is_floating_point_v<decltype(details::X(std::declval<T>()))>>
How template specialization works:
There is a primary specialization. This one basically defines the arguments and defaults.
template <typename T, typename = void>
This is the template part of your primary specialization. It takes one type, then another type that defaults to void.
This is the "interface" of your template.
template <typename T>
[...] <T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>> [...]
here is a secondary specialization.
In this case, the template <typename T> is fundamentally different. In the primary specialization, it defined an interface; here, it defines "variables" that are used below.
Then we have the part where we do the pattern matching. This is after the name of the template (variable in this case). Reformatted for sanity:
<
T,
enable_if_t
<
is_floating_point_v
<
decltype
(
details::X(T())
)
>,
T
>
>
now we can see the structure. There are two arguments, matching the two arguments in the primary specialization.
The first one is T. Now, this matches the name in the primary specialization, but that means nothing. It is like calling a function make_point(int x, int y) with variables x,y -- it could be y,x or m,n and make_point doesn't care.
We introduced a completely new variable T in this specialization. Then we bound it to the first argument.
The second argument is complex. Complex enough that it is in a "non-deduced context". Typically, template specialization arguments are deduced from the arguments passed to template as defined in the primary specialization; non-deduced arguments are not.
If we do some_template< Foo >, matching a type T against Foo gets ... Foo. Pretty easy pattern match. Fancier pattern matches are permitted, like a specialization that takes a T*; this fails to match against some_template<int>, but matches against some_template<int*> with T=int.
Non-deduced arguments do not participate in this game. Instead, the arguments that do match are plugged in, and the resulting type is generated. And if and only if that matches the type passed to the template in that slot does the specialization match.
So lets examine what happens we pass vec as the first argument to my_temp
First we go to the primary specialization
template<typename T, typename=void>
my_temp
now my_temp<vec> has a default argument. It becomes my_temp<vec,void>.
We then examine each other specialization to see if any of them match; if none do, we stay as the primary specialization.
The other specialization is:
template<typename T>
[...] my_temp<
T,
enable_if_t
<
is_floating_point_v
<
decltype
(
details::X(T())
)
>,
T
>
>[...]
with [...] for stuff that doesn't matter.
Ok, the first argument is bound to T. Well, the first argument is vec, so that is easy. We substitute:
template<typename T>
[...] my_temp<
vec,
enable_if_t
<
is_floating_point_v
<
decltype
(
details::X(vec())
)
>,
vec
>
>[...]
then evaluate:
template<typename T>
[...] my_temp<
vec,
enable_if_t
<
is_floating_point_v
<
double
>,
vec
>
>[...]
and more:
template<typename T>
[...] my_temp<
vec,
enable_if_t
<
true,
vec
>
>[...]
and more:
template<typename T>
[...] my_temp<
vec,
vec
>[...]
ok, remember we where trying to match against my_temp<vec,void>. But this specialization evaluated to my_temp<vec,vec>, and those don't match. Rejected.
Remove the ,T from enable_if, or make it ,void (same thing), and the last line of the above argument becomes my_temp<vec,void> matches my_temp<vec,void>, and the secondary specialization is chosen over the primary one.
It is confusing. The same syntax means fundamentally different things in primary specialization and secondary ones. You have to understand pattern matching of template arguments and non-deduced contexts.
And what you usually get is someone using it like a magic black box that you copy.
The magic black box -- the patterns -- are useful because they mean you don't have to think about the details of how you got there. But understanding pattern matching of template arguments, deduced and non-deduced contexts, and the differences between primary and secondary specializations is key to get why the black box works.
With
struct vec
{
double x;
double y;
double z;
};
and
template <typename T>
constexpr double X(const T& param) { return param.x; }
we'll find out that
is_floating_point_v<decltype(details::X(T()))
evaluates to true (unless you're going to specialise X for vec not to return floating point...).
So we actually have:
template <typename T>
constexpr details::subscript_function<T> my_temp<T, enable_if_t<true, T>>[]
= { /*...*/ };
or shorter:
template <typename T>
constexpr details::subscript_function<T> my_temp<T, T>[]
= { /*...*/ };
(if it exists at all, of course...). Explicitly choosing one or the other:
my_temp<decltype(foo), void>
my_temp<decltype(foo), int>
my_temp<decltype(foo), double>
all match the main template, but none of the specialisation.
my_temp<decltype(foo), decltype(foo)>
now does match the specialisation (which exists because of X(foo) returning double...).
Finally back to my_temp<decltype(foo)> – well, only one template parameter given. Which is the type of the second one? The default parameter tells you (or better: the compiler), it is void. And according to above...
So if you want to match the specialisation, either this one needs void as type of second template parameter (as you discovered already) or you change the default in the non-specialized template to being equal to first template parameter (typename T, typename = T).
Actually, you could select any type for the default and the specialisation, as long as you choose the same for both (e. g. twice int, std::string, MyVeryComplexCustomClass, ...).
Amazed (and cofused) by a similar question I tried myself the example that the mentioned question found in the standard:
template <typename T, typename U = int> struct S;
template <typename T = int, typename U> struct S
{ void f() { std::cout << __PRETTY_FUNCTION__ << '\n'; } };
int main()
{
S s; s.f();
return 0;
}
The code above prints void S<int, int>::f() [T = int, U = int] compiled with gcc HEAD 8.0.1 201803 but fails to compile with clang HEAD 7.0.0 unless angle brackets are used during instantiation:
S s; s.f(); // error: declaration of variable 's' with deduced type 'S' requires an initializer
S<> t; t.f(); // Correct
This issue aside, I've checked the other template flavors for this particular behavior and the code is accepted or rejected in a pretty irregular manner:
Template function
template <typename T, typename U = int> void function();
template <typename T = int, typename U> void function()
{ std::cout << __PRETTY_FUNCTION__ << '\n'; }
int main()
{
/* Rejected by GCC: no matching function for call to 'function()'
template argument deduction/substitution failed:
couldn't deduce template parameter 'T'
same error with function<>()
CLang compiles without issues */
function(); // CLang prints 'void function() [T = int, U = int]'
return 0;
}
Template variable
template <typename T, typename U = int> int variable;
template <typename T = int, typename U> int variable = 0;
int main()
{
/* GCC complains about wrong number of template arguments (0, should be at least 1)
while CLang complains about redefinition of 'variable' */
std::cout << variable<> << '\n';
return 0;
}
Template alias
template <typename T, typename U = int> using alias = int;
template <typename T = int, typename U> using alias = int;
int main()
{
/* GCC complains about redefinition of 'alias'
while CLang compiles just fine. */
alias<> v = 0;
std::cout << v << '\n';
return 0;
}
The standards text about this feature doesn't tell apart the different template types so I thought that they should behave the same.
But yet, the template variable case is the one rejected by both compilers, so I have some doubts about the template variable option. It makes sense to me that CLang is right rejecting the template variable complaining about redefinition while GCC is wrong by rejecting the code for the wrong reasons, but this reasoning doesn't follow what the standard says in [temp.param]/10.
So what should I expect for the case of template variables?:
Code rejected due to redefinition (CLang is right).
Code accepted, merging both template definitions (Both GCC and CLang are wrong).
For class template argument deduction, this is a clang bug. From [temp.param]/14:
The set of default template-arguments available for use is obtained by merging the default arguments from all prior declarations of the template in the same way default function arguments are ([dcl.fct.default]). [ Example:
template<class T1, class T2 = int> class A;
template<class T1 = int, class T2> class A;
is equivalent to
template<class T1 = int, class T2 = int> class A;
— end example ]
When you write S s, both template arguments are defaulted, and so the rewrite of the default constructor is:
template <typename T=int, typename U=int>
S<T, U> __f();
Which is viable and should deduce S<int, int>.
For functions, you do not need to specify <> to call a function template if all the template parameters can be deduced. This is [temp.arg.explicit]/3:
If all of the template arguments can be deduced, they may all be omitted; in this case, the empty template argument list <> itself may also be omitted.
Note that this applies to deduction. There is no deduction for alias templates or variable templates. Hence, you cannot omit the <>. This is [temp.arg]/4:
When template argument packs or default template-arguments are used, a template-argument list can be empty. In that case the empty <> brackets shall still be used as the template-argument-list.
Disclaimer: the following is valid in context of C++14. With C++17 both compilers are wrong. See the another answer by Barry.
Looking into details I see that Clang is correct here, while GCC is confused.
First case, class template (unlike function template) indeed requires <>.
Second case, function template, is treated by Clang exactly like the first case, without the syntactic requirement to use <> to indicate that template is used. This applies in C++ to function templates in all contexts.
Third case: as of variables, I see that Clang is correct while GCC is confused. If you redeclare the variable with extern Clang accepts it.
template <typename T, typename U = int> int variable = 0;
template <typename T = int, typename U> extern int variable;
int main()
{
// accepted by clang++-3.9 -std=c++14
std::cout << variable<> << '\n';
return 0;
}
so it again behaves consistently both with standard and the previous cases. Without extern this is redefinition and it is forbidden.
Fourth case, using template. Clang again behaves consistently. I used typeid to ensure that alias is indeed int:
template <typename T, typename U = int> using alias = int;
template <typename T = int, typename U> using alias = int;
int main()
{
alias<> v = 0;
std::cout << v << '\n';
std::cout << typeid(v).name() << '\n';
return 0;
}
then
$ ./a.out | c++filt -t
outputs
0
int
So Clang indeed makes no difference of what kind of template is re-declared, as stated in the standard.
When using SFINAE to select constructor overloads in the past, I have typically used the following:
template <typename T>
class Class {
public:
template <typename U = T, typename std::enable_if<std::is_void<U>::value, int>::type=0>
Class() {
std::cout << "void" << std::endl;
}
template <typename U = T, typename std::enable_if<!std::is_void<U>::value, int>::type=0>
Class() {
std::cout << "not void" << std::endl;
}
};
However, I just came across this alternative:
template <typename U = T, typename std::enable_if<std::is_void<U>::value>::type...>
Class() {
std::cout << "void" << std::endl;
}
Considering that the following is illegal ...
template <typename U = T, void...> // ERROR!
Class() { }
... how does the alternative above using ellipses rather than a non-type template argument work?
Full code: http://coliru.stacked-crooked.com/a/64a1aaf13ce6099b
My previous answer was wrong. Sorry. I'm just going to fix it.
This declaration:
template <typename U = T, void...>
Class() { }
violates [temp.res]/8:
The program is
ill-formed, no diagnostic required, if [...] every valid specialization of a variadic template requires an empty template parameter pack
It's no diagnostic required, but the compiler chooses to issue one anyway. Either way, the code is ill-formed.
On the other hand
template <typename U = T, std::enable_if_t<std::is_void<U>::value>...>
Class() { }
doesn't violate this requirement. We have an empty pack, so we don't run afoul of the fact that you can't use void as a non-type template parameter. Additionally, a hypothetical specialization of enable_if could provide a type that isn't void there so it's not ill-formed due to the above limitation.
This is the code:
#include <iostream>
#include <type_traits>
template <class T>
typename std::enable_if<std::is_integral<T>::value,bool>::type
is_odd (T i) {return bool(i%2);}
// 2. the second template argument is only valid if T is an integral type:
template < class T,
class = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even (T i) {return !bool(i%2);}
int main() {
short int i = 1; // code does not compile if type of i is not integral
std::cout << std::boolalpha;
std::cout << "i is odd: " << is_odd(i) << std::endl;
std::cout << "i is even: " << is_even(i) << std::endl;
return 0;
}
I am trying to learn proper usage of enable_if, i understand if it's used as a return type specifier is that: the compiler will ignore the code. Meaning, the function won't be in the binary file.
I am somehow confused if it's used in a template argument. Based from the code above, it says there that the second template argument is only valid if T is an integral type But i am confused what is the purpose of that second argument?
I removed it and changed it to:
template < class T>
bool is_even (T i) {return !bool(i%2);}
and it still works fine. Can someone clarify me what's the real purpose of it? There's no variable specifier on it as well.
Or maybe it only serves as a checker if ill do something like
template < class T,
class B= typename std::enable_if<std::is_integral<T>::value>::type>
Allowing me to access B on my code(can be true or false) ?
You are using it with an integral type. It will only fail when used with a non-integral type. The following should fail to compile:
float f;
is_even(f);
Note that with a C++14 compiler you can write:
template <class T,
class = std::enable_if_t<std::is_integral<T>::value>>
However, you may want to use a static_assert here, since the function probably does not make sense for non integral types, and it will give a better error message at compile time.
Another usage of additional parameters in SFINAE is to lower the preference for a given function. The template with more template arguments with be less likely to get selected than the one with less templates arguments.
The purpose of the enable_if in this case is to cause a compilation error when the deduced template argument for T is not an integral type. We can see from cppreference/is_integral that integral types are integers, characters and their signed and unsigned variants.
For any other type you will get an error that looks like:
main.cpp:21:32: error: no matching function for call to 'is_odd'
std::cout << "i is odd: " << is_odd(NotIntegral()) << std::endl;
^~~~~~
main.cpp:6:25: note: candidate template ignored: disabled by 'enable_if' [with T = NotIntegral]
typename std::enable_if<std::is_integral<T>::value,bool>::type
I understand if it's used as a return type specifier is that the compiler will ignore the code
This isn't true. The return type is evaluated just like any other part of the declaration. See Why should I avoid std::enable_if in function signatures. The choice of placement of the std::enable_if has its pros and cons.
But i am confused what is the purpose of that second argument?
Consider the example where you have a function named foo that takes some T.
template<class T> void foo(T);
You want to restrict one overload of foo whose T's have a value member equal to 1, and want to use another overload for when T::value does not equal 1. If we simply have this:
template<class T> void foo(T); // overload for T::value == 1
template<class T> void foo(T); // overload for T::value != 1
How does this convey to the compiler that you want to use two separate overloads for two separate things? It doesn't. They are both ambiguous function calls:
template<std::size_t N>
struct Widget : std::integral_constant<std::size_t, N> { };
int main() {
Widget<1> w1;
Widget<2> w2;
foo(w1); // Error! Ambiguous!
foo(w2); // Error! Ambiguous!
}
You will need to use SFINAE to reject the templates based on our condition:
template<class T, class = std::enable_if_t<T::value == 1>* = nullptr>
void foo(T); // #1
template<class T, class = std::enable_if_t<T::value != 1>* = nullptr>
void foo(T); // #2
Now the correct ones are called:
foo(w1); // OK! Chooses #1
foo(w2); // OK! Chooses #2
What makes this work is the way std::enable_if is built. If the condition in its first parameter is true, it provides a member typedef named type equal to the second parameter (defaulted to void). Otherwise, if the condition is false, it does not provided one. A reasonable implementation could be:
template<bool, class R = void>
struct enable_if { using type = R; };
template<class R>
struct enable_if<false, R> { /* empty */ };
So if the condition fails, we will be trying to access a ::type member that doesn't exist (remember that std::enable_if_t is an alias for std::enable_if<...>::type). The code would be ill-formed, but instead of it causing a hard error, during overload resolution the template is simply rejected from the candidate set and other overloads (if present) are used instead.
This is a simple explanation of SFINAE. See the cppreference page for more information.
In your case there is only one overload of is_odd and is_even, so if you pass NotIntegral to them, they will fail because their respective overloads are taken out of the candidate set, and since there are no more overloads to evaluate overload resolution fails with the aforementioned error. This could be cleaned up a bit with a static_assert message instead.
template<class T>
bool is_even (T i) {
static_assert(std::is_integral<T>::value, "T must be an integral type");
return !bool(i%2);
}
Now you will no longer be given a weird looking error message but your own custom one. This also rules out SFINAE since the static_assert is evaluated after template arguments are substituted, so it's your choice on which route to take. I would recommend the assertion since it's much cleaner and there is no need for SFINAE here.