Compile-Time If-Expressions for Types in C++11
The other day I wondered if there is a way to metaprogam a syntax that returns
a type based on a conditional expression akin to an if
…else if
…else
expression.
The idea came to me when I searched for a programmatic way to choose the right
size of integer for a bitset, given a certain number of bits. And yes, I know
there’s std::bitset
, but I’ll take any excuse to investigate an interesting metaprogramming problem.
Simple Conditionals
The standard library already gives us a metafunction
std::conditional<C,T,F>
,
which is the semantic equivalent of a ternary operator for types: if the
(compile time known) boolean condition C
evaluates to true
we get the
type T
, otherwise F
. There’s nothing like an else if
logic in there,
but it is still instructive to see how to implement this ourselves.
We go about this by first defining a structure template Conditional
like so
template<bool Condition, typename T, typename E>
struct Conditional {}
Then we specialize this for the boolean cases true
and false
, respectively:
template<typename T, typename E>
struct Conditional<true,T,E> {
using type = T;
}
template<typename T, typename E>
struct Conditional<false,T,E> {
using type = E;
}
template<bool Condition, typename T, typename E>
using Conditional_t = Conditional<Condition,T,E>::type;
All of this might look unwieldy at first, but is from the standard metaprogramming bag of tricks. Now we can use this expression to switch a type based on a compile time known condition like so:
// assuming BIT_COUNT is compile time known
using MyType = Conditional_t<BIT_COUNT <= 8,int8_t,int64_t>;
This conditional enables simple logic with only two branches. Furthermore,
without looking up how Conditional
(or std::conditional
for that matter) works,
I don’t find this code very readable1.
So I set out to do something both more powerful and more readable.
The Goal
A few years ago I have implemented if expressions for values,
which are also constexpr
. So they work for compile time known values, but not
for types. However, I like the syntax and I wanted to see whether I can come
up with a similar syntax that allows us to choose types at compile time.
The syntax I ended up implementing looks like this2:
using IntType = If<(BIT_COUNT <= 8)>
::Then<uint8_t>
::ElseIf<(BIT_COUNT <= 16)>
::Then<uint16_t>
::ElseIf<(BIT_COUNT <= 32)>
::Then<uint32_t>
::Else<uint64_t>;
IntType my_integer = 0;
Whether this construct is useful in other circumstances than this particular example I don’t know. But I find it very readable and was excited to challenge myself with the implementation.
We can build on the general idea of the Conditional
implementation above
which uses specialization to select for types. Additionally, we can draw
inspiration from the if expressions implementation,
where we have created transient types for the if, then, else if,
and else parts of the expression.
The Implementation
Let’s have a look at the implementation and then walk through it. I suggest to only skim this code for now and jump to the next paragraph where we walk through the code in detail. The layout of the code is so that the compiler is happy. The declarations need to be in a particular order that is less intuitive to a human reader.
// (1)
template <bool Condition, typename T>
struct ThenType {};
// (2)
template <bool IfCondition,
typename T,
bool ElifCondition>
struct ElifType {
// (3)
template <typename E>
using Then = typename Conditional<IfCondition,
ThenType<true, T>,
ThenType<ElifCondition, E>
>::type;
};
// (4)
template <typename T>
struct ThenType<true, T> {
//(5)
template <typename E>
using Else = T;
// (6)
template <bool ElifCondition>
using ElseIf = ElifType<true, T, ElifCondition>;
};
// (7)
template <typename T>
struct ThenType<false, T> {
// (8)
template <typename E>
using Else = E;
// (9)
template <bool ElifCondition>
using ElseIf = ElifType<false, T, ElifCondition>;
};
// (10)
template <bool Condition>
struct If {
// (11)
template <typename T>
using Then = ThenType<Condition, T>;
};
Let’s start at the bottom with the ⑩ If
structure template,
which takes a compile time known boolean condition Condition
. It is the entry
point to our metafunction and the key trick here is that we have an associated
templated type ⑪ Then<T>
. It evaluates to the ThenType<Condition,T>
type,
which we’ll discuss in a moment. We can think about this templated associated
type like a function in type space. It is only possible with using
declarations
in C++11 and up. Before that we would have had to use a typedef
and I know
of no way to make the typedef generic on a template parameter.
The ① ThenType<C,T>
structure template remembers the condition C
from
the original ⑩ If<C>
and takes a type T
that should be returned if
this C
is true. If we look at the template specializations ④ ThenType<true,T>
and ⑦ ThenType<false,T>
we can see that we have ⑤⑧Else<E>
associated types that take a type parameter E
. Since else is the last piece
of the chain, this Else
metafunction evaluates to a concrete type depending on
the condition. If the condition is true, it will evaluate to T
, which is the
original type from the if condition, and otherwise it will evaluate to E
, which
is the type given to Else
. These pieces enable us to write If<(X<3)>::Then<int>::Else<float>
.
This is already more readable than std::conditional
to my mind, but we want to go further.
To enable the else if part of the expression we add ⑥⑨
ElseIf
associated types in the ThenType
structure template specializations.
This type is the reason we have to have this weird out of order declaration of ThenType
in the first place. You surely noticed that we declared the ThenType
template on top,
then the ElifType
template and only after that we specialized the ThenType
template
for the possible outcomes of the condition. This is because we want to use ThenType
from ElifType
but also ElifType
from ThenType
. Since the compiler needs to see those
declarations before usage, we help it by essentially forward declaring the ThenType
template. ② ElifType
has one member metafunction ③ Then
which uses our
Conditional
in such a way that we propagate the correct logic and types through the chain.
Conclusions and Code
In summary, we have enabled a syntax for using an if…else if…else logic to expressively select types at compile time based on logical conditions in C++11. This nicely complements my previous if expressions implementation, which we can use to select values (but not types) at compile time. Both implementations are available as part of my func++ repository repository on GitHub.
Endnotes
-
I know we can nest
std::conditional
to get an else if logic but that only exacerbates the problems with readability. ↩ -
For my first try I used
constexpr
functions on theIf
type which can be made to return (instances of) different types usingif constexpr
. There’s many problems with this approach, but the biggest problem is that we have to extract the return type usingdecltype
, which does not make for a pretty syntax ↩
Comments
You can comment on this post using your GitHub account.
Join the discussion for this article on this ticket. Comments appear on this page instantly.