Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheriting from standard library type traits

When writing custom type traits, I often derive them from the standard library type traits as follows:

template<typename T>
struct some_type_trait:
    std::is_arithmetic<T>
{};

However, I sometimes wonder whether inheriting from the type member type of the standard library type trait is cleaner:

template<typename T>
struct some_type_trait:
    std::is_arithmetic<T>::type
{};

The overall idea is that only the inheritance from std::bool_constant matters in the end, but the fact the we're inheriting from std::is_arithmetic in the first example and not directly from std::bool_constant (as in the second case) is observable via polymorphism or utilies like std::is_base_of.

The gist is that inheriting directly from bool_constant via type member type feels cleaner because it is exactly what we want to. However, inheriting from std::is_arithmetic is slightly shorter and provides essentially the same behaviour. So... is there any subtle advantage that I might be missing when picking one or the other (correctness, compile time...)? Are there subtle scenarios where inheriting from std::is_arithmetic might change the behaviour of the application, compared to inheriting directly from the underlying bool_constant?

like image 363
Morwenn Avatar asked Apr 19 '17 10:04

Morwenn


1 Answers

The second has the minor pitfall of leaking implementation details; someone can test via function overloads if you inherit from is_arithmetic<int> or whatever. This might "work" and lead to false positives.

This is an exceedingly minor issue.

You may get slightly better diagnostics by inheriting from is_arithmetic if your compiler dumps the base class name.

Neither of your designs composes well. Instead:

template<class T>
struct some_type_trait:
  std::integral_constant<bool,
    std::is_arithmetic<T>{}
  >
{};

can be extended, as we can put any expression in there.

As I noted earlier, having the types in the error message can be helpful, so we could do:

constexpr bool all_of() { return true; }
template<class...Bools>
constexpr bool all_of(bool b0, Bools...bs) {
    return b0 && all_of(bs...);
}
template<class T, template<class...>class...Requirements>
struct Requires : std::integral_constant<bool,
  Requirements<T>{} &&...
  // in C++11/14, something like: all_of(Requirements<T>::value...)
> {};

Then we get:

template<class T>
using some_type_trait = Requires<T, std::is_arithmetic>;

which if it fails to find an overload in a tag dispatch, will generate an error that might give you a clue.

template<class T>
void test( std::true_type passes_test, T t ) {
  std::cout << t+0 << "\n";
}
template<class T>
void test(T t) {
  return test(some_type_trait<T>{}, t);
}
int main() {
  test(3);
  test("hello");
}

Sadly we don't have the equivalent of easy binding/partial application/currying in template metaprogramming. So f<.> = is_base_of<X, .> is hard to express tersely.

Live example, the error messages we get is: clang:

main.cpp:23:10: note: candidate function [with T = const char *] not viable: no known conversion from 'some_type_trait<const char *>' (aka 'Requires<const char *, std::is_arithmetic>') to 'std::true_type' (aka 'integral_constant<bool, true>') for 1st argument

gcc:

main.cpp:28:18: note:   cannot convert 'Requires<const char*, std::is_arithmetic>()' (type 'Requires<const char*, std::is_arithmetic>') to type 'std::true_type {aka std::integral_constant<bool, true>}'

which at least leads you to the error (that const char* is not is_arithmetic).

like image 195
Yakk - Adam Nevraumont Avatar answered Oct 18 '22 14:10

Yakk - Adam Nevraumont