Building a C++ SIMD Abstraction (4/N) – Type Traits Are Your Friend

In this post I want to share with you how type traits can be an asset to C++ template code. Given that tsimd is based on a core template over a collection of values stored in a SIMD register, there’s plenty of motivation for finding ways to keep the interface and implementation easy to use and difficult to misuse. Obviously these are goals for better code and not a hard set of rules, but I think type traits can definitely aid template code moving toward these goals.

So what are type traits?

Type traits are templates which let you infer compile-time information about a give type or instance. C++11 introduced a number of type traits that come in the standard library (found in the “type_traits” header, reference here), but type traits are also tools which library makers can write themselves to solve specific problems found in library development.

In my experience, type traits come in two flavors: those which generate a value based on given type(s) and those which generate an output type from given type(s). By convention, those types which generate some value result from their input types define a static member called ‘value’. These values could be technically anything, but I almost always compute a simple boolean condition with them. Also by convention, types which generate new types from an input type define a type alias member called ‘type’. I use these conventions in my own type traits to be consistent with the standard library and I highly recommend that you do the same…we don’t want to remember more than we have to!

Why would I use type traits?

Given the above flavors of type traits, I find myself using them in the following situations.

Type inference

In implementing tsimd I found myself needing to map multiple input types into another set of types. Here, given a pack, where ‘T’ is the element type of the pack and ‘W’ is the width, I could easily use a type trait which maps the pair to a particular output type. Here’s a concrete example:

In this example, I use a type trait to infer which intrinsic type should represent the pack. It’s OK if you don’t know what a ‘__m256’ actually is because that is exactly what tsimd is trying to hide from you! For the sake of some context of how this gets used, here’s a stripped down version of the pack class definition which highlights just part of the simd_type usage.

Thus you can see that with one type trait to infer what the underlying intrinsic type should be, only one declaration is required for the different contexts which a pack may interact with an intrinsic type.

There are a few lurking advantages here too, one of which I’ll point out: because intrinsic_t turns into a dummy type in situations where there the intrinsic type doesn’t make sense, some interfaces intentionally become invalid. In the case of the constructor from intrinsic_t to pack (as declared above), when compiling for AVX you can’t construct a pack with a single __m128 (an SSE register), even though it is a valid type for pack. Furthermore, if you use a pack, the union storing the various representations of the data will silently turn the ‘v’ member into nothing. Therefore (for the most part) all of this gets implemented exactly once: no need to duplicate definitions of pack for different ISAs.

Type validation

Another case for using type traits is to validate a template parameter against a particular condition or set of conditions. Here’s a simple example:

Simply put, same_size takes in two types and defines a compile-time boolean whether the two types are the same size in bytes.

Another example is detecting if a template type (such as pack) is an instantiation of that template, without having to infer each of the template arguments. One way to solve this is to use inheritance and the std::is_base_of type trait. In tsimd, pack does this with the following (simplified, of course).

I’ve found the use of an empty base class as a way to detect general usage of a template to be useful in a number of places, include both tsimd and OSPRay.

SFINAE (Substituion Failure Is Not An Error)

The last case I find myself using type traits is to make SFINAE a more bearable thing. SFINAE is about culling certain overloads from being candidates during overload resolution. To some of you that may sound confusing, but hopefully an example will help. Consider the following plus operator between a pack and scalar type:

The problem with the above example is that OTHER_T needs to be convertible to float (the element type in vfloat8) in order for operator+() to be considered between vfloat8 and OTHER_T. I won’t go into tons of detail about how many ways you can use SFINAE (lookup std::enable_if examples if you want to see how ugly it can get), but by far the most readable way of using SFINAE is to use an extra default argument at the end of the template parameter list which uses a single type trait which encapsulates everything that OTHER_T can be. The way this will look is as follows.

Note that the enable_if alias (valid_pack_scalar_operator_t) is a standard pattern in the STL, where a given trait will give you a “{trait_name}_t” version to use for this very purpose. So now that we have the above valid_pack_scalar_operator_t type defined, we can constrain the operator+() template as follows.

Thus the overall goal is achieved: a template which can take anything gets appropriately culled down to only the types which it expects without the need to explicitly write out every version of the template manually. Furthermore, other overloads could also be introduced without this version colliding with it. In other words, if someone else wanted to define operator+() which takes any container type, one could use a similar strategy, but with a type trait which SFINAEs out OTHER_T which isn’t a container….etc, etc.

SFINAE is a complicated topic in itself, but if you build up type traits which directly express (by giving it a name) a particular predicate of interest (i.e. are the given type(s) I am interested in OK for this template or not?) will help you keep things straight and somewhat readable. Yes, I know that C++ concepts will improve this, but that is not yet a mainstream enough feature implement in enough compilers that I can use it right now (it’s not even standardized yet!)…so as of today, this is what I’ve got.

Conclusion

Type traits come in two flavors: one which define types and others which define a compile-time values. With these you can do useful things to improve your C++ template code like checking to make sure template types are well defined according to your template’s assumptions. I hope that you find type traits useful in making your template code easier to use and difficult to misue.

As tsimd continues to mature, there will be more to talk about…so the series is definitely not over. The hiatus from writing was certainly not intentional, I’m just busy!