
Revelations Metaprogrammer. We program the program code at the compilation stage, use C ++ templates for non-standard solutions
- Tutorial

Templates can be called the most important difference and the main advantage of the C ++ language. The ability to create an algorithm template for various types without copying code and with strict type checking is just one aspect of using templates. The template specialization code is built at the compilation stage, which means that the behavior of the created types and functions can be controlled. How can one resist the possibility of programming compiled classes?
Metaprogramming is becoming as integral to writing C ++ code as using a standard library, some of which was created specifically for use at the compilation stage. Today we will be producing a library of safe casts of scalar types C ++, metaprogramming with templates!
Template break
In fact, all metaprogramming comes down not so much to template behavior, regardless of type, as to breaking this behavior template itself. Suppose we have a template class or template function:
template
class Some;
template
T func(T const& value);
As a rule, such classes and functions are described immediately with a body common to any type. But no one is stopping us from setting an explicit specialization of the template for one of the types, creating a unique function behavior for this type or a special type of class:
template <>
class Some
{
public:
explicit Some(int value)
: m_twice(value * 2) {
}
int get_value() const {
return m_twice / 2;
}
private:
int m_twice;
};
template <>
double func(double const& value)
{
return std::sqrt(value);
}
In this case, the general behavior can be described very differently from the template specified for the specializations:
template
class Some
{
public:
explicit Some(T const& value)
: m_value(value) {
}
T const& get_value() const {
return m_value;
}
private:
T m_value;
};
template
T func(T const& value)
{
return value * value;
}
In this case, when using the template, special behavior will be observed for the `Some` and` func` specializations: it will strongly differ from the general behavior of the template, although the API will differ slightly externally. But when creating instances of `Some` will store the double value and return the original value, dividing the` m_twice` property in half at the request of `get_value ()`. The general template `Some`, where T is any type except int, will just save the passed value, giving a constant reference to the` m_value` field with every request to `get_value ()`.
The func function calculates the root of the argument value at all, while any other specialization of the func template will calculate the square of the passed value.
Why is this needed? As a rule, in order to make a logical fork in the template algorithm, for example, this:
template
T create()
{
Some some(T());
return func(some.get_value());
}
The behavior of the algorithm inside create will be different for int and double types. In this case, the behavior of various components of the algorithm will differ. Despite the inconsistency of the template specialization code, we got a simple and understandable example of managing template behavior.
Break a nonexistent template
Let's make our example a little more fun - we’ll remove the general behavior pattern for Some and func, leaving only the Some and func specializations already written and, of course, without touching the preliminary announcement.
What will happen to the create template in this case? It just stops compiling for any type. Indeed, for `create` there is no implementation of the function` func`, and for `create` there is no necessary` Some`. The very first attempt to insert create into a code for some type will result in a compilation error.
To leave the `create` function working, you need to specialize` Some` and `func` from at least one type at a time. You can implement `Some` or` func`, for example like this:
template <>
int func(int const& value)
{
return value;
}
template <>
class Some
{
public:
explicit Some(double value)
: m_value(value*value) {
}
double get_value() const {
return m_square;
}
private:
double m_square;
};
By adding two specializations, we not only revived the compilation of create specializations from the int and double types, it also turned out that the algorithm would return the same values for these types. But the behavior will be different!
INFO
In C ++, types behave differently and not always a template algorithm behaves efficiently for all types. Often, adding the specialization of the template, we get not only a performance increase, but also a more understandable behavior of the program as a whole.
Yes, help us std ::
Every year, more and more metaprogramming tools are added to the standard library. As a rule, everything new is a well-tested old one, borrowed from the Boost.MPL library and legalized. More and more often we need `#include
The auxiliary structures of the template are built in such a way that only that specialization is compiled, which gives the truth of the expression, and there are no specializations for false behavior.
To make it clearer, let's take a closer look at `std :: enable_if`. This template depends on the truth of its first argument (the second is optional), and an expression of the form `std :: enable_if :: type` will be compiled only for true expressions, this is done quite simply - by specializing from true:
template
struct enable_if;
template
struct enable_if
{
typedef result_type type;
};
For false of type `std :: enable_if
:: type` the compiler simply cannot create, and this can be used, for example, by restricting the behavior of a number of types of partial specialization of a template structure or class.
Here, a variety of predicate structures from the same `can be used as arguments to` std :: enable_if`
Well, the introductory part is completed, let's move on to practice.How are predicates arranged?
A predicate is the usual partial specialization of a template structure. For example, for `std :: is_same` in general, everything looks something like this:template
For matching argument types of `std :: is_same`, the C ++ compiler will choose the appropriate specialization, in this case partial with value = true, and for mismatched ones it will fall into the general implementation of the template with value = false. The compiler always tries to find a strictly suitable specialization by types of arguments and, just not finding the right one, goes to the general implementation of the template.Template entry is strictly prohibited
To start programming program code and do all kinds of metaprogramming, let's try to create a scary function that returns different results for the same and different types of template arguments. The partial specialization mechanism for the auxiliary structure will help us in this. Since there is no partial specialization for functions, inside the function we will simply refer to a simple corresponding specialization of the structure, for which we will set the partial specialization:template
Obviously, we have created a preset for the safe cast function. The function is based on the types of arguments passed to it and goes to execute the static try_cast method with the corresponding specialization of the type_cast structure. At the moment, we have implemented only the trivial case when the type of value coincides with the type of result and conversion, in fact, is not necessary. The input variable is simply assigned to the result variable, and true is always returned - a sign of the successful conversion of the value type to the type of the result.
For mismatched types, a compilation error with long incomprehensible text will now be issued. To fix this a bit, you need to introduce a general template implementation with `static_assert (false, ...)` in the body of the `try_cast` method - this will make the error message more clear:template
Thus, every time an attempt is made to cast the type with the try_safe_cast function to types for which there is no corresponding specialization of the type_cast structure, a compilation error message will be generated from the general template.
The workpiece is ready, it's time to start metaprogramming!Mark me here!
First you need to fix the declaration of the auxiliary structure `type_cast`. We will need the additional type `meta_type` for a logical fork without compromising the parameters passed and implicitly defining their types. Now the description of the structure template will look a little more complicated:template
As you can see, the new type in the template declaration is optional and does not interfere with the existing declarations of specialization and general template behavior. However, this small nuance allows us to control the compilation success by passing the result of `std :: enable_if <predicate> :: value` as the third parameter. Specializations with an uncompiled parameter of the template will be discarded, which is what we need to control the logic of casting types of different groups.
After all, it is obvious that integers are reduced to each other in different ways, depending on whether both types have a sign, which type has a greater bit depth, and whether the transferred value is out of the range of acceptable values for `result_type`.
So, if both types are signed integers and the type of the result is more digitized than the type of the input value, then it is possible to assign the input value to the result without problems, the same is true for unsigned types. Let's describe this behavior with a special partial specialization of the `type_cast` template:template
Now we need to figure out what kind of condition we need to insert instead of the ellipsis with the parameter `std :: enable_if`.
Let's go describe the compilation time condition:typename std::enable_if<
Firstly, specialization should not intersect with the existing one, where the type of result and input value are the same:!std::is_same
Secondly, we consider the case when both arguments of the template are integer types:std::is_integral
Thirdly, we mean that both types are either signed or unsigned (brackets are required - the conditions of the template parameters are calculated differently than at the execution stage!):(std::is_signed
Fourth, the width of the integer type of the result is greater than the width of the type of the transmitted value (again, the brackets are required!):(sizeof(result_type) > sizeof(value_type))
Finally, close the std :: enable_if declaration:::type
As a result, type for `std :: enable_if` will be generated only if the above four conditions are met. In other cases, for other type combinations, this partial specialization will not even be created.
It turns out a furious expression inside `std :: enable_if`, which cuts off exclusively the case we specified. This template saves from copying the code of casting various integer types to each other.
To consolidate the material, we can describe a slightly more complex case - casting an unsigned integer to a type of lower bit depth of an unsigned integer. Here, knowledge of the binary representation of an integer and the standard class `std :: numeric_limits` will help us:template
In the if condition, everything is quite simple: the maximum value of the `result_type` type is implicitly cast to a type greater than the bit of` value_type` and acts as a mask for the value of `value`. If for the value `value` bits are used outside of the` result_type`, we will get the inequality satisfied and get to return false.
Now let's go through the compile-time condition:typename std::enable_if<
The first two conditions remain the same - both types are integer, but different from each other:!std::is_same
Both types are unsigned integers:std::is_unsigned
The type of the result is less bit than the type of the input value (brackets are required!):(sizeof(result_type) < sizeof(value_type))
All conditions are listed, close the specialization condition:::type
For signed integers, where the result is less than a bit, the condition will be similar, but with two `std :: is_signed` inside` std :: enable_if`, however the condition of going beyond the values will be slightly different:static bool try_cast(result_type& result, value_type const& value)
{
if (value != (value &
(std::numeric_limits
Again, recall the binary representation of signed integers: here the mask will be the bit sign of the input value and the bits of the value of the result type, excluding the sign bit. Accordingly, the minimum number of type `value_type`, where only the sign bit is filled, is combined bitwise with the maximum number of type` result_type`, where all bits except the signed one are filled, and will give us the desired mask of valid values.
As homework, consider the following cases:
It is also not difficult to write similar specializations for converting from `std :: is_floating_point` types, as well as converting from` bool` type. For complete satisfaction, you can add a cast from and to string types and make this a much needed library of safe C ++ type casting.Thinking outside the box
An exception may exist for each use of the template. Now you will be ready to meet with him and competently process it. You don’t always need a special metatype in the auxiliary structure template, but if it’s time to process the predicates at the compilation stage, then there’s nothing wrong with that. All you need to do is roll up your sleeves and carefully create a template design with a compile-time predicate.
But be careful, the abuse of templates is not good! Treat templates solely as a generalization of code for different types with similar behavior, templates should appear reasonably when there is a risk of duplicating the same code for different types.
Remember also that in order to understand the logic of a template predicate without a code author, you need to be at least a bold optimist, so take care of the psyche of your colleagues, make template predicates neatly, beautifully and readably and do not be shy to comment on almost every condition in the predicate .
Template the code carefully and only when necessary, and your colleagues will thank you. And do not be afraid to break the template in case of exclusion from the rules. Rules without exceptions are, rather, exceptions to the rules.
First published in Hacker Magazine # 193.
Posted by Vladimir Qualab Kerimov, C ++ Lead Parallels Developer
Subscribe to Hacker