Reputation: 22552
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
?
Upvotes: 15
Views: 1013
Reputation: 275270
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
).
Upvotes: 3