10

Is there any reason why the standard specifies them as template structs instead of simple boolean constexpr?

In an additional question that will probably be answered in a good answer to the main question, how would one do enable_if stuff with the non-struct versions?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • 2
    "Why isn't it done with a simple X?" "How could it be done with X?" Lulz. :) – Lightness Races in Orbit Jan 17 '12 at 14:55
  • `enable_if` is a metafunction that returns a type. You can't return types from regular functions. Only the type traits that produce integral constants can be made `constexpr` functions. – R. Martinho Fernandes Jan 17 '12 at 15:01
  • 1
    I'm afraid I don't understand what alternative you're suggesting by "simple boolean `constexpr`". How are you suggesting that, say, `std::is_integral` could be declared but wasn't? I suspect the answer has to do with the fact that it's possible to overload a function on `true_type` and `false_type`, but it's not possible to overload a function on the value of a `constexpr`, and this overloading provides an extra tool for TMP. But I'm not sure -- if you want to know why `is_integral` etc are class templates, then I don't see how they could be anything else (other than built-in operators). – Steve Jessop Jan 17 '12 at 15:01
  • 1
    "how would one do enable_if stuff with the non-struct versions?" - exactly. – Steve Jessop Jan 17 '12 at 15:06

5 Answers5

20

One reason is that constexpr functions can't provide a nested type member, which is useful in some meta-programming situations.

To make it clear, I'm not talking only of transformation traits (like make_unsigned) that produce types and obviously can't be made constexpr functions. All type traits provide such a nested type member, even unary type traits and binary type traits. For example is_void<int>::type is false_type.

Of course, this could be worked around with std::integral_constant<bool, the_constexpr_function_version_of_some_trait<T>()>, but it wouldn't be as practical.

In any case, if you really want function-like syntax, that is already possible. You can just use the traits constructor and take advantage of the constexpr implicit conversion in integral_constant:

static_assert(std::is_void<void>(), "void is void; who would have thunk?");

For transformation traits you can use a template alias to obtain something close to that syntax:

template <bool Condition, typename T = void>
using enable_if = typename std::enable_if<Condition, T>::type;
// usage:
// template <typename T> enable_if<is_void<T>(), int> f();
//
// make_unsigned<T> x;
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • Actually, a function can yield a type. It is commonly done to check base-to-derived relationships for exemple: you produce two overloads of the function, with different return type, and this return type is used. – Matthieu M. Jan 17 '12 at 16:28
  • 2
    ... grrr ... hate that freeze ... Anyway: `decltype` allows to get types out of functions painlessly: `decltype(is_void(std::declval()))` vs `typename is_void::type`... both look very similar to me. – Matthieu M. Jan 17 '12 at 16:37
  • 2
    Wow template aliases are more expressive than I first thought! – deft_code Jan 18 '12 at 03:40
  • Your last example exists in the stdlib now, as `std::enable_if_t`, alongside various other equivalent helpers for other type traits. – underscore_d Sep 25 '16 at 19:16
5

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.

Xeo
  • 129,499
  • 52
  • 291
  • 397
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • I don't quite get your argument that "inheritance just works" with the `constexpr` based functions. Mind expanding the answer with an example? – Xeo Jan 17 '12 at 18:59
  • @Xeo: done (near the end of the answer). Now it really starts looking like an endless rant :p – Matthieu M. Jan 18 '12 at 07:26
  • I see. However, that would mean passing the type you're interested in as a function argument, not as a template argument, so you'd need `std::declval` all over the place and hope that the context is unevaluated. – Xeo Jan 18 '12 at 07:53
  • @Xeo: potentially yes. It really changes the way you use the type traits obviously. For example, if you want to instrument `template void foo(T const& t)`, then you can use: `template auto foo(T const& t) -> typename std::enable_if::type`. No need for an unevaluated context, you *have* the argument already :) – Matthieu M. Jan 18 '12 at 08:01
  • I know it's an year later, but I recently realised something relevant to this question (we are all still discovering C++11, right?) and came to edit my answer and have seen yours... "a good number of traits have just become redundant." This may or may not be true, but I don't think the example given is a good one. iterator_traits is still not redundant because the types there do not have to be exact matches to the natural decltype counterparts. – R. Martinho Fernandes Jan 31 '13 at 15:49
3

One reason is that the type_traits proposal is older than the constexpr proposal.

Another one is that you are allowed to add specializations for your own types, if needed.

Bo Persson
  • 90,663
  • 31
  • 146
  • 203
2

Probably because boost already had a version of type_traits that was implemented with templates.

And we all know how much people on the standards committee copy boost.

  • 10
    "Copying boost" sounds silly, when the sole reason for founding boost was to provide a testbed for new stuff for the standard library. To have stuff from it "copied" into the std lib is what boost is for. See [here](http://www.boost.org/users/proposal.pdf) (PDF link). – sbi Jan 17 '12 at 14:53
  • 5
    You don't have to "copy" when the same people are members of both Boost and the standards committee. :-) – Bo Persson Jan 17 '12 at 14:55
  • @sbi: The PDF you linked to doesn't indicate whatsoever that this was "the sole reason". In fact it makes quite clear that uptake into the stdlib may not happen for any given library, but that a conventional uptake might one day happen to lead to it. – Lightness Races in Orbit Jan 17 '12 at 14:57
  • @Lightness: Boost was started by members of the library working group, with the aim to gather libraries to become existing practice, and propose some of them for standardization. It might well be that Beman's original paper doesn't say this very explicitly (I haven't read it in a decade), but that doesn't change the fact that this is true. I'm sure somewhere in boost's docs (or their FAQ, if they have such a thing) this is spelled out. – sbi Jan 17 '12 at 15:08
  • @sbi: It may well be, but I'm just pointing out that your citation is invalid. :) (Citing it when you haven't read it in ten years seems a bit odd!) – Lightness Races in Orbit Jan 17 '12 at 16:04
2

I would say the mainreason is that type_traits was already part of tr1 and was therefore basically guaranteed to end up in the standard in more or less the same form, so it predates constexpr. Other possible reasons are:

  • Having the traits as types allows for overloading functions on the type of the trait
  • Many traits (like remove_pointer) define a type instead of a value, so they have to be expressed in this way. Having different interfaces for traits defining values and traits defining types seems unnessecary
  • templated structs can be partial specialized, while functions can't, so that might make the implementation of some traits easier

For your second question: As enable_if defines a type (or not, if it is passed false) a nested typedef inside a struct is really the way to go

Grizzly
  • 19,595
  • 4
  • 60
  • 78
  • FTR, the only template in `` can be explicitly specialized in a program is `common_type`. – R. Martinho Fernandes Jan 17 '12 at 15:17
  • @Grizzly: Functions can not be partially specialized but they can be overloaded. Instead of having a nested trait, you can just use the return type of the function... and I didn't understood the first point. – Matthieu M. Jan 17 '12 at 16:30
  • @MatthieuM.: Using the returntype of the function (which could not really return anything) would mean always using `decltype` for traits declaring a type, which seems pretty ugly. I do realize that functions can be overleaded, but how would that help for defining traits, which would probably not take (non template) arguments, since all arguments are per definition types instead of values. What do you mean you didn't understand the first point? `type_traits` was in `tr1` and therefore predates `constexpr` – Grizzly Jan 17 '12 at 17:01
  • @Grizzly: I was speaking of "Having the traits as types allows for overloading functions on the type of the trait". As for traits, you can use `std::declval()` to get a value (in a non-evaluated context), so you can then have `false_type foo(T)` be invoked. But of course switching to constexpr would probably mean that you have `constexpr bool foo(T) { return false; }` and *if needed*, convert back to a type `std::bool_type`. You are reversing the work when assuming that traits only work on types, once you go the constexpr way this hack disappears :) – Matthieu M. Jan 17 '12 at 18:13