Note: this ends up looking more like a rant than a proper answer... I did got some itch reading the previous answers though, so please excuse me ;)
First, class traits are historically done with template structures because they predate constexpr
and decltype
. Without those two, it was a bit more work to use functions, though the various library implementations of is_base_of
had to use functions internally to get the inheritance right.
What are the advantages of using functions ?
- inheritance just works.
- syntax can be more natural (
typename ::type
looks stupid TM)
- a good number of traits are now obsolete
Actually, inheritance is probably the main point against class traits. It's annoying as hell that you need to specialize all your derived classes to do like momma. Very annoying. With functions you just inherit the traits, and can specialize if you want to.
What are the disadvantages ?
- packaging! A struct trait may embed several types/constants at once.
Of course, one could argue that this is actually annoying: specializing iterator_traits
, you just so often gratuitously inherit from std::iterator_traits
just to get the default. Different functions would provide this just naturally.
Could it work ?
Well, in a word where everything would be constexpr
based, except from enable_if
(but then, it's not a trait), you would be going:
template <typename T>
typename enable_if<std::is_integral(T()) and
std::is_signed(T())>::type
Note: I did not use std::declval
here because it requires an unevaluated context (ie, sizeof
or decltype
mostly). So one additional requirement (not immediately visible) is that T
is default constructible.
If you really want, there is a hack:
#define VALUE_OF(Type_) decltype(std::declval<T>())
template <typename T>
typename enable_if<std::is_integral(VALUE_OF(T)) and
std::is_signed(VALUE_OF(T))>::type
And what if I need a type, not a constant ?
decltype(common_type(std::declval<T>(), std::declval<U>()))
I don't see a problem either (and yes, here I use declval
). But... passing types has nothing to do with constexpr
; constexpr
functions are useful when they return values that you are interested in. Functions that return complex types can be used, of course, but they are not constexpr
and you don't use the value of the type.
And what if I need to chain trais and types ?
Ironically, this is where functions shine :)
// class version
template <typename Container>
struct iterator { typedef typename Container::iterator type; };
template <typename Container>
struct iterator<Container const> {
typedef typename Container::const_iterator type;
};
template <typename Container>
struct pointer_type {
typedef typename iterator<Container>::type::pointer_type type;
};
template <typename Container>
typename pointer_type<Container>::type front(Container& c);
// Here, have a cookie and a glass of milk for reading so far, good boy!
// Don't worry, the worse is behind you.
// function version
template <typename Container>
auto front(Container& c) -> decltype(*begin(c));
What! Cheater! There is no trait defined!
Hum... actually, that's the point. With decltype
, a good number of traits have just become redundant.
DRY!
Inheritance just works!
Take a basic class hierarchy:
struct Base {};
struct Derived: Base {};
struct Rederived: Derived {};
And define a trait:
// class version
template <typename T>
struct some_trait: std::false_type {};
template <>
struct some_trait<Base>: std::true_type {};
template <>
struct some_trait<Derived>: some_trait<Base> {}; // to inherit behavior
template <>
struct some_trait<Rederived>: some_trait<Derived> {};
Note: it is intended that the trait for Derived
does not state directly true
or false
but instead take the behavior from its ancestor. This way if the ancestor changes stance, the whole hierarchy follows automatically. Most of the times since the base functionality is provided by the ancestor, it makes sense to follow its trait. Even more so for type traits.
// function version
constexpr bool some_trait(...) { return false; }
constexpr bool some_trait(Base const&) { return true; }
Note: The use of ellipsis is intentional, this is the catch-all overload. A template function would be a better match than the other overloads (no conversion required), whereas the ellipsis is always the worst match guaranteeing it picks up only those for which no other overload is suitable.
I suppose it's unnecessary to precise how more concise the latter approach is ? Not only do you get rid of the template <>
clutter, you also get inheritance for free.
Can enable_if
be implemented so ?
I don't think so, unfortunately, but as I already said: this is not a trait. And the std
version works nicely with constexpr
because it uses a bool
argument, not a type :)
So Why ?
Well, the only technical reason is that a good portion of the code already relies on a number of traits that was historically provided as types (std::numeric_limit
) so consistency would dictate it.
Furthermore it makes migration from boost::is_*
just so easier!
I do, personally, think it is unfortunate. But I am probably much more eager to review the existing code I wrote than the average corporation.