Consider the following, basic example based on void_t
:
template>
struct S: std::false_type {};
template&l
This is the metaprogramming equivalent of: should I write a function or should I just write my code inline. The reasons to prefer to write a type trait are the same as the reasons to prefer to write a function: it's more self-documenting, it's reusable, it's easier to debug. The reasons to prefer to write trailing decltype are the similar to the reasons to prefer to write code inline: it's a one-off that isn't reusable, so why put in the effort factoring it out and coming up with a sensible name for it?
But here are a bunch of reasons why you might want a type trait:
Suppose I have a trait I want to check lots of times. Like fooable
. If I write the type trait once, I can treat that as a concept:
template
struct fooable : std::false_type {};
template
struct fooable().foo())>>
: std::true_type {};
And now I can use that same concept in tons of places:
template {}>* = nullptr>
void bar(T ) { ... }
template {}>* = nullptr>
void quux(T ) { ... }
For concepts that check more than a single expression, you don't want to have to repeat it every time.
Going along with repetition, composing two different type traits is easy:
template
using fooable_and_barable = std::conjunction, barable>;
Composing two trailing return types requires writing out all of both expressions...
With a type trait, it's easy to check that a type doesn't satisfy a trait. That's just !fooable
. You can't write a trailing-decltype
expression for checking that something is invalid. This might come up when you have two disjoint overloads:
template ::value>* = nullptr>
void bar(T ) { ... }
template ::value>* = nullptr>
void bar(T ) { ... }
which leads nicely into...
Assuming we have a short type trait, it's a lot clearer to tag dispatch with a type trait:
template void bar(T , std::true_type fooable) { ... }
template void bar(T , std::false_type not_fooable) { ... }
template void bar(T v) { bar(v, fooable{}); }
than it would be otherwise:
template auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
template void bar(T v, ... ) { ... }
template void bar(T v) { bar(v, 0); }
The 0
and int/...
is a little weird, right?
static_assert
What if I don't want to SFINAE on a concept, but rather just want to hard fail with a clear message?
template
struct requires_fooability {
static_assert(fooable{}, "T must be fooable!");
};
When (if?) we ever get concepts, obviously actually using concepts is much more powerful when it comes to everything related to metaprogramming:
template void bar(T ) { ... }