For this entry, I would like to introduce the type_traits
header file. This file contains utility templates that greatly simplify work when writing template-based libraries. This is especially true for libraries that employ template meta-programming. The header file is available with C++ TR1 and above. This library provides tools to identify types, their qualifying properties and even peel-off properties one-by-one programmatically at compile-time. There are also transformation meta-functions that provide basic meta-programming operations such as a compile-time conditional.
The definitions in type_traits
will save us a lot of time implementing Alchemy. As I introduce some of the tools that are available in the header, I will also demonstrate how these operations can be implemented. This will help you understand how to construct variations of the same type of solution when applying it in a different context. As an example of this, I will create a construct that behaves similarly to the tertiary operator, but is evaluated at compile-time.
Types and Values
Creating C++ template meta-programs are essentially functional programs. Functional programs compute with mathematical expressions. You will receive the same result each time you call a function with a specific set of parameters. As expressions are declarative, state and mutable data are generally avoided. Meta-programs are structured in a way to require the compiler to calculate the results of the expressions as part of compilation. The two constructs that we have to work with are integer constants, to hold calculation results, and types, which we can use like function calls.
Define a Value
Meta-program constants are declared and initialized statically. Therefore, these values are limited to integer-types. We cannot use string
literals or floating-points because these types of static
constants cannot be initialized in place with the class
or struct
definition. Sometimes, you will see code declared with enumerations. I believe this is to prevent meta-programming objects from using space. It is possible for users to take the address of static
constants, and if this occurs, the compiler must allocate space for the constant, even if the object is never instantiated. It is not possible to take the address of an enumeration declaration. Therefore no memory must be allocated when an enumeration is used.
Since I started using type_traits
, I don't worry about it so much. I use the integral_constant
template to define values. By convention, values are given the name value
. This is important in generic programming to allow the object development to remain loosely coupled and independent of the design of other objects. The example below demonstrates how the integral_constant
is typically used. Please note that all of these constructs live in the std::
namespace, which I will be omitting in these examples.
template < size_t BaseT, size_t ExpT >
struct Pow
: integral_constant< size_t,
BaseT * Pow< BaseT , ExpT-1>::value >
{ };
template < size_t BaseT >
struct Pow< BaseT, 1 >
: integral_constant< size_t, BaseT >
{ };
template < size_t BaseT >
struct Pow< BaseT, 0 >
: integral_constant< size_t, 1 >
{ };
Call (Instantiate) our meta-function:
int x = Result< 3,3 >::value;
integral_constant
All that is required to implement the integral_constant
, is a definition of a static
constant in the template struct
. Struct
s are generally used because of their default public
member access. Here is the most basic implementation:
template < typename T,
T ValT>
struct integral_constant
{
static const T value = ValT;
};
Yes it's that simple. The implementation in the C++ Standard Library goes a little bit further for convenience. Just as with the STL Containers, typedef
s are created for the value_type
and type
of the object. There is also a value conversion operator to implicitly convert the object to the value_type
. Here is the complete implementation:
template < typename T,
T ValT>
struct integral_constant
{
typedef T value_type;
typedef integral_constant< T, ValT> type;
static const T value = ValT;
operator value_type() const
{
return value;
}
};
Two typedef
s have been created to reduce the amount of code required to perform Boolean logic with meta-programs:
typedef integral_constant< bool, true > true_type;
typedef integral_constant< bool, false > false_type;
Compile-time Decisions
With meta-programming, there is only one way to define a variable, and there are many ways to create decision making logic. Let's start with one that is very useful for making decisions.
is_same
This template allows you to test if two types are the same type.
template < typename T,
typename U >
struct is_same
: false_type
{ };
template < typename T >
struct is_same< T,T >
: true_type
{ };
The compiler always looks for the best fit. That way, when multiple templates would be suitable, only the best match will be selected, if that is possible. In this case, the best match for when both types are the exact same, is our specialization that indicates true
.
conditional
It's time to define a branching construct to enable us to make choices based on type. The conditional
template is the moral equivalent of the if
-statement for imperative C++.
template < bool Predicate,
typename T,
typename F >
struct conditional
{
typedef F type;
};
template < typename T,
typename F >
struct conditional < true, T, F >
{
typedef T type;
};
Applying These Techniques
I have just demonstrated how three of the constructs defined in the type_traits
header could be implemented. The techniques used to implement these constructs are used repeatedly to create solutions for evermore complex problems. I would like to demonstrate a construct that I use quite often in my own code, which is both built upon the templates I just discussed, and implemented with the same techniques used to implement those templates.
value_if
While the conditional
template will define a type based on the result a Boolean expression, I commonly want to conditionally define a value based on the result of a Boolean expression. Therefore, I implemented the value_if
template. This makes use of the integral_constant
template and a similar implementation as was used to create the conditional
template. This gives me another tool to simplify the complex parametric expressions that I often encounter.
template < bool Predicate,
typename T,
T TrueValue,
T FalseValue >
struct value_if
: integral_constant< T, FalseValue >
{ };
template < typename T,
T TrueValue,
T FalseValue >
struct value_if < true, T, TrueValue, FalseValue >
: integral_constant< T, TrueValue >
{ };
Summary
I just introduced you to the type_traits
header in C++. If you have not yet discovered this header, you should check it out. It can be very useful, even if you are not creating template meta-programs. Here is a reference link to the header from cppreference.com[^].
With the basic constructs that I introduced in this entry, I should now be able to create more sophisticated ways to interact with the Typelist[^] that I previously discussed for Alchemy. With the simple techniques used above, we should be able to implement template expressions that will query a Typelist
type by index, get the size of the type at an index, and similarly calculate the offset in bytes from the beginning of the Typelist
. The offset will be one of the most important pieces of information to know for the Alchemy implementation.