Using C++ templates for size-based type selection

The standardisation of size-specific integer types in C/C++ is extremely useful for portability. That is, when you use types like uint16_t and int32_t, you know exactly what size of data type you’re getting (assuming your compiler supports it, which is usually the case unless you’re working with very specialist/embedded systems). This isn’t the case with the more traditional types like short and int whose sizes can vary from one compiler to another. However, the size is obviously part of the type name, meaning you need to alter declarations directly if you want to use a type of a different size.

Using the std::conditional template from C++11 lets you change this. Instead, you can have a template parameter which specifies the size you want (e.g. in bytes). The corresponding type declaration can be automatically deduced from there.

 

The code

This makes use of a ‘using‘ declaration (similar to a templated typedef). Within it are three nested conditional templates:

template <std::uint8_t T_numBytes>
using UintSelector =
    typename std::conditional<T_numBytes == 1, std::uint8_t,
        typename std::conditional<T_numBytes == 2, std::uint16_t,
            typename std::conditional<T_numBytes == 3 || T_numBytes == 4, std::uint32_t,
                std::uint64_t
            >::type
        >::type
    >::type;

Example usage:

UintSelector<6> var = 185;

A more complete example is available here on ideone.

The relationships between sizes and types is:

  • 1 byte => std::uint8_t
  • 2 bytes => std::uint16_t
  • 3-4 bytes => std::uint32_t
  • 5+ bytes => std::uint64_t

One major limitation to note is that this only goes up to 8 bytes (64 bits). Anything higher will still give you std::uint64_t. If you want to handle anything bigger then you’ll need some other approach, such as using an array instead of a scalar type. Alternatively, you may want the compiler to emit an error if anything larger is attempted.

std::conditional

The workhorse of the code is the conditional template. It was introduced in C++11, although it was certainly possible to implement it manually before that. The form is as follows:

std::conditional< bool condition, typename ifTrue, typename ifFalse >

The conditional class has a typedef inside named type. If the condition is true then the typedef is defined as the ifTrue template parameter. Otherwise, it is defined as ifFalse. (These are arbitrary names which aren’t specified in the standard.) In the underlying code, this could be done through partial template specialisation.

As in the example above, the conditionals can be nested. If the condition is true then it resolves to a specific type. Otherwise, it resolves to another conditional, and so on until you reach the final conditional in the chain.

Note that you have must provide a valid ifFalse template parameter. If you only want ifTrue then you might want to look into std::enable_if instead.

One important thing to be aware of in the code above is the need to prefix the conditional with typename. Without it, you’ll get a compiler error saying something like “dependent name is not a type”. This Stack Overflow question has some excellent information about this issue in the answers.

What’s it good for?

I originally explored this while tinkering with ideas for my avidmath library (which is still extremely early in development at this time). I’ve been looking at implementing a runtime-resizeable equivalent of std::bitset, which would form the basis for arbitrary sized integers and fixed point types. The idea is that the data would be split up into chunks, allowing various binary operations to happen chunk-at-a-time instead of having to iterate over every individual bit.

Hypothetically, a programmer using one of the classes would be able to specify the chunk size that would be the fastest computationally, and/or the least wasteful in terms of memory. In practice, I’m not sure it will make a lot of difference though. The increased complexity of the code (and the increased likelihood of bugs) might outweigh the marginal performance benefits.

There are other places this could come in handy too, such as selecting an integral type whose size most closely resembles another type (such as a class). Perhaps this could be useful in serialization for network transmission.

Other approaches

In most cases, it would make more sense just to specify the desired type directly in code or as a template parameter. The only real benefit I can think of for size-based selection is ensuring a particular category of type is used, such as signed vs. unsigned. You can currently use type traits and static asserts to guard against that though, and hopefully we might see concepts make a come-back in a future version of the standard.

Leave a Reply

Your email address will not be published. Required fields are marked *