Which <type_traits> cannot be implemented without compiler hooks?
Asked Answered
A

2

41

C++11 provides standard <type_traits>.

Which of them are impossible to implement without compiler hooks?

  • Note 1: by compiler hook I mean any non-standard language feature such as __is_builtin....
  • Note 2: a lot of them can be implemented without hooks (see chapter 2 of C++ Template Metaprogramming and/or chapter 2 of Modern C++ Design).
  • Note 3: spraff answer in this previous question cites N2984 where some type traits contain the following note: is believed to require compiler support (thanks sehe).
Arron answered 24/11, 2013 at 22:7 Comment(24)
@rightfold is_void does not seem to need a lot of magic :)Niple
@Niple Just out of genuine curiosity, how would you implement is_void?Moersch
Now the question is whether a "hook" must be compiled into your compiler, or whether it can be provided as part of your toolchain's stdlib implementation. At that point, of course, it's just code. See what I mean?Coset
I thought that std::is_pod<T> can be implemented without a compiler hook! Something along the lines of not being a pointer or a reference but a viable argument for a variable argument list or something.Stainless
@H2CO3: It's trivial: template <typename> struct is_void: std::true_type {}; template <> struct is_void<void>: false_type {};Stainless
@H2CO3 I've checked there to have a reliable implementation before answering, and here it relies on integral_constant, is_same and remove_cv the 3 of them being implementable without compiler magic.Niple
Maybe relevant: N2518 -- Compiler support for type traitsDanica
@DietmarKühl trivial but no so :) It need to handle "cv" also.Niple
Actually, an answer to this question would need to prove that it is impossible for each of the > 70 type traits to be implemented w/o compiler support.Danica
@Niple It's easy enough to add specialisations for all possible qualifiers.Jaye
@Johan: fair enough, there need to be three more specializations or some hackery removing the qualifiers. The basic principle stays the same.Stainless
@H2CO3: just specialize the possibilities, surely? template<typename T> struct is_void : integral_constant<bool, false> {};, and then for V equal to each cv-qualified version of void (so, 4 specializations) template<> struct is_void<V> : integral_constant<bool, true> {};. For integral_constant in "pure" C++ without <type_traits> see the possible implementation at en.cppreference.com/w/cpp/types/integral_constantFides
@hvd Indeed but I prefer the cppreference version using remove_cv. :)Niple
@DietmarKühl Passing non-"POD" class types to an ellipsis is conditionally-supported, [expr.call]/7.Danica
is_enum ?Sempstress
is_polymorphic comes to mind, as does underlying_type, and is_bind_expression.Beside
@Arron It might be better to ask the question which of the type traits can be implemented w/o compiler support, as you'd just have to provide an exemplary implementation.Danica
@AaronMcDaid Maybe it's possible to check if the type is a valid non-type template parameter: an enum is, but a class is not. So you could check if it's not an integral type but a valid non-type template parameter.Danica
@KerrekSB boost has a "non-portable" check is_polymorphic by testing the size of a derived class after adding a virtual function.Danica
@DietmarKühl: for what it's worth, pointer types are POD.Fides
@DyP: Didn't we just discuss earlier that this doesn't work for non-polymorphic classes with virtual bases?Beside
@KerrekSB You seem to have discussed that issue, I didn't participate in that discussion ;) But yes, it indeed seems to be a problem.Danica
is_function can't be written in standard C++ because of extern "C" and other language linkages.Edvard
@Edvard functions have the special property that adding const to them doesn't change the type. This can be exploited in combination with is_reference, since functions aren't references.Favus
R
34

I have written up a complete answer here — it's a work in progress, so I'm giving the authoritative hyperlink even though I'm cutting-and-pasting the text into this answer.

Also see libc++'s former documentation page on Type traits intrinsic design (mirrored in Apple's old docs here).

is_union

is_union queries an attribute of the class that isn't exposed through any other means; in C++, anything you can do with a class or struct, you can also do with a union. This includes inheriting and taking member pointers.

is_aggregate, is_literal_type, is_pod, is_standard_layout, has_virtual_destructor

These traits query attributes of the class that aren't exposed through any other means. Essentially, a struct or class is a "black box"; the C++ language gives us no way to crack it open and examine its data members to find out if they're all POD types, or if any of them are private, or if the class has any constexpr constructors (the key requirement for is_literal_type).

is_abstract

is_abstract is an interesting case. The defining characteristic of an abstract class type is that you cannot get a value of that type; so for example it is ill-formed to define a function whose parameter or return type is abstract, and it is ill-formed to create an array type whose element type is abstract. (Oddly, if T is abstract, then SFINAE will apply to T[] but not to T(). That is, it is acceptable to create the type of a function with an abstract return type; it is ill-formed to define an entity of such a function type.)

So we can get very close to a correct implementation of is_abstract using this SFINAE approach:

template<class T, class> struct is_abstract_impl : true_type {};
template<class T> struct is_abstract_impl<T, void_t<T[]>> : false_type {};

template<class T> struct is_abstract : is_abstract_impl<remove_cv_t<T>, void> {};

However, there is a flaw! If T is itself a template class, such as vector<T> or basic_ostream<char>, then merely forming the type T[] is acceptable; in an unevaluated context this will not cause the compiler to go instantiate the body of T, and therefore the compiler will not detect the ill-formedness of the array type T[]. So the SFINAE will not happen in that case, and we'll give the wrong answer for is_abstract<basic_ostream<char>>.

This quirk of template instantiation in unevaluated contexts is the sole reason that modern compilers provide __is_abstract(T).

is_final

is_final queries an attribute of the class that isn't exposed through any other means. Specifically, the base-specifier-list of a derived class is not a SFINAE context; we can't exploit enable_if_t to ask "can I create a class derived from T?" because if we cannot create such a class, it'll be a hard error.

is_empty

is_empty is an interesting case. We can't just ask whether sizeof (T) == 0 because in C++ no type is ever allowed to have size 0; even an empty class has sizeof (T) == 1. "Emptiness" is important enough to merit a type trait, though, because of the Empty Base Optimization: all sufficiently modern compilers will lay out the two classes

struct Derived : public T { int x; };

struct Underived { int x; };

identically; that is, they will not lay out any space in Derived for the empty T subobject. This suggests a way we could test for "emptiness" in C++03, at least on all sufficiently modern compilers: just define the two classes above and ask whether sizeof (Derived) == sizeof (Underived). Unfortunately, as of C++11, this trick no longer works, because T might be final, and the "final-ness" of a class type is not exposed by any other means! So compiler vendors who implement final must also expose something like __is_empty(T) for the benefit of the standard library.

is_enum

is_enum is another interesting case. Technically, we could implement this type trait by the observation that if our type T is not a fundamental type, an array type, a pointer type, a reference type, a member pointer, a class or union, or a function type, then by process of elimination it must be an enum type. However, this deductive reasoning breaks down if the compiler happens to support any other types not falling into the above categories. For this reason, modern compilers expose __is_enum(T).

A common example of a supported type not falling into any of the above categories would be __int128_t. libc++ actually detects the presence of __int128_t and includes it in the category of "integral types" (which makes it a "fundamental type" in the above categorization), but our simple implementation does not.

Another example would be vector int, on compilers supporting Altivec vector extensions; this type is more obviously "not integral" but also "not anything else either", and most certainly not an enum type!

is_trivially_constructible, is_trivially_assignable

The triviality of construction, assignment, and destruction are all attributes of the class that aren't exposed through any other means. Notice that with this foundation we don't need any additional magic to query the triviality of default construction, copy construction, move assignment, and so on. Instead, is_trivially_copy_constructible<T> is implemented in terms of is_trivially_constructible<T, const T&>, and so on.

is_trivially_destructible

For historical reasons, the name of this compiler builtin is not __is_trivially_destructible(T) but rather __has_trivial_destructor(T). Furthermore, it turns out that the builtin evaluates to true even for a class type with a deleted destructor! So we first need to check that the type is destructible; and then, if it is, we can ask the magic builtin whether that destructor is indeed trivial.

underlying_type

The underlying type of an enum isn't exposed through any other means. You can get close by taking sizeof(T) and comparing it to the sizes of all known types, and by asking for the signedness of the underlying type via T(-1) < T(0); but that approach still cannot distinguish between underlying types int and long on platforms where those types have the same width (nor between long and long long on platforms where those types have the same width).

Roman answered 21/5, 2017 at 7:22 Comment(1)
The "Type traits intrinsic design" link does not work. Could you please update it?Impressionist
S
23

Per lastest boost documentation which is also a bit old but I think it's valid still

Support for Compiler Intrinsics

There are some traits that can not be implemented within the current C++ language: to make these traits "just work" with user defined types, some kind of additional help from the compiler is required. Currently (April 2008) Visual C++ 8 and 9, GNU GCC 4.3 and MWCW 9 provide at least some of the the necessary intrinsics, and other compilers will no doubt follow in due course.

The Following traits classes always need compiler support to do the right thing for all types (but all have safe fallback positions if this support is unavailable):

is_union
is_pod
has_trivial_constructor
has_trivial_copy
has_trivial_move_constructor
has_trivial_assign
has_trivial_move_assign
has_trivial_destructor
has_nothrow_constructor
has_nothrow_copy
has_nothrow_assign
has_virtual_destructor

The following traits classes can't be portably implemented in the C++ language, although in practice, the implementations do in fact do the right thing on all the compilers we know about:

is_empty
is_polymorphic

The following traits classes are dependent on one or more of the above:

is_class 
Secret answered 24/11, 2013 at 22:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.