About the Author Peter Gottschling’s professional passion is writing leading-edge scientific software, and
Chapter 3. Generic Programming Templates are a feature of C++ to create functions and classes that operate on parametric
3.4 Type Deduction and Definition C++ compilers already deduce types automatically in C++03 for arguments of function
templates. Let f be a template function and we call f(g(x, y, z) + 3 * x) Then the compiler can deduce the type of f’s argument.
3.4.1 Automatic Variable Type
When we assign the result of an expression like the preceding one to a variable, we need to know the type of this expression in C++03. On the other hand, if we assign to a type to which the result is not convertible, the compiler will let us know while providing the incompatible types. This shows that the compiler knows the type, and in C++11, this knowledge is shared with the programmer.The easiest way to use the type information in the previous example is the auto-matic variable type:
Click here to view code image
auto a= f(g(x, y, z) + 3 * x);
This does not change the fact that C++ is strongly typed. The auto type is different from dynamic types in other languages like Python. In Python, an assignment to a can change the type of a, namely, to that of the assigned expression. In C++11, the variable a has the type of the expression’s result, and this type will never change afterward. Thus, the auto
type is not an automatic type that adapts to everything that is assigned to the variable but is determined once only.
We can declare multiple auto variables in the same statement as long as they are all initialized with an expression of the same type: Click here to view code image auto i= 2 * 7.5, j= std::sqrt(3.7); // okay: both are double auto i= 2 * 4, j= std::sqrt(3.7); // Error: i is int, j double auto i= 2 * 4, j; // Error: j not initialized auto v= g(x, y, z); // result of f
Click here to view code image
auto& ri= i; // reference on i
const auto& cri= i; // constant reference on i
auto&& ur= g(x, y, z); // forward reference to result of f
The type deduction with auto variables works exactly like the deduction of function parameters, as described in Section 3.1.2. This means, for instance, that the variable v is not a reference even when g returns a reference. Likewise, the universal reference ur is either an rvalue or an lvalue reference depending on the result type of f being an rvalue or lvalue (reference).
3.4.2 Type of an Expression
The other new feature in C++11 is decltype. It is like a function that returns the type of an expression. If f in the first auto example returns a value, we could also express it with
decltype: Click here to view code image decltype(f(g(x, y, z) + 3 * x)) a= f(g(x, y, z) + 3 * x); Obviously, this is too verbose and thus not very useful in this context. The feature is very important in places where an explicit type is needed: first of all as a template parameter for class templates. We can, for instance, declare a vector whose elements can hold the sum of two other vectors’ elements, e.g., the type of v1[0] + v2[0]. This allows us to express the appropriate return type for the sum of two vectors of different types: Click here to view code image template <typename Vector1, typename Vector2> auto operator+(const Vector1& v1, const Vector2& v2) -> vector< decltype(v1[0] + v2[0]) >; This code snippet also introduces another new feature: Trailing Return Type. In C++11, we are still obliged to declare the return type of every function. With decltype, it can be more handy to express it in terms of the function arguments. Therefore, we can move the declaration of the return type behind the arguments.
The two vectors may have different types and the resulting vector yet another one. With the expression decltype(v1[0] + v2[0]) we deduce what type we get when we add elements of both vectors. This type will be the element type for our resulting vector.
An interesting aspect of decltype is that it only operates on the type level and does not evaluate the expression given as an argument. Thus, the expression from the previous example does not cause an error for empty vectors because v1[0] is not performed but only its type is determined.
The two features auto and decltype differ not only in their application; the type deduction is also different. While auto follows the rules of function template parameters and often drops reference and const qualifiers, decltype takes the expression type as
it is. For instance, if the function f in our introductory example returned a reference, the variable a would be a reference. A corresponding auto variable would be a value.
As long as we mainly deal with intrinsic types, we get along without automatic type detection. But with advanced generic and meta-programming, we can greatly benefit from these extremely powerful features.
3.4.3 decltype(auto)
This new feature closes the gap between auto and decltype. With
decltype(auto), we can declare auto variables that have the same type as with
decltype. The following two declarations are identical:
Click here to view code image
decltype(expr) v= expr; // redundant + verbose when expr long decltype(auto) v= expr; // Ahh! Much better.
The first statement is quite verbose: everything we add to expr we have to add twice in the statement. And with every modification we must pay attention that the two expressions are still identical.
c++14/value_range_vector.cpp
The preservation of qualifiers is also important in automatic return types. As an
example we introduce a view on vectors that tests whether the values are in a given range. The view will access an element of the viewed vector with operator[] and return it after the range test with exactly the same qualifiers. Obviously a job for decltype(auto). Our example implementation of this view only contains a constructor and the access operator: Click here to view code image template <typename Vector> class value_range_vector { using value_type= typename Vector::value_type; using size_type= typename Vector::size_type; public: value_range_vector(Vector& vref, value_type minv, value_type maxv) : vref(vref), minv(minv), maxv(maxv) {} decltype(auto) operator[](size_type i) { decltype(auto) value= vref[i]; if (value < minv) throw too_small{}; if (value > maxv) throw too_large{}; return value; } private: Vector& vref; value_type minv, maxv; };
returned. Both the type of the temporary and the return type are deduced with
decltype(auto). To test that vector elements are returned with the right type, we store one in a decltype(auto) variable and inspect its type:
Click here to view code image int main () { using Vec= mtl::vector<double>; Vec v= {2.3, 8.1, 9.2}; value_range_vector<Vec> w(v, 1.0, 10.0); decltype(auto) val= w[1]; }
The type of val is double& as wanted. The example uses decltype(auto) three times: twice in the view implementation and once in the test. If we replaced only one of them with auto, the type of val would become double.
3.4.4 Defining Types
There are two ways to define types: with typedef or with using. The former was introduced in C and existed in C++ from the beginning. This is also its only advantage: backward compatibility.7 For writing new software without the need of compiling with pre-11 compilers, we highly recommend you
7. This is the only reason why examples in this book sometimes still use typedef.
Advice
Use using instead of typedef.
It is more readable and more powerful. For simple type definitions, it is just a question of order:
typedef double value_type;
versus
using value_type= double;
In a using declaration, the new name is positioned on the left while a typedef puts it on the right side. For declaring an array, the new type name is not the right-most part of a
typedef and the type is split into two parts:
typedef double da1[10];
In contrast to it, within the using declaration, the type remains in one piece:
using da2= double[10];
The difference becomes even more pronounced for function (pointer) types—which you will hopefully never need in type definitions. std::function in §4.4.2 is a more
that returns a float reads Click here to view code image typedef float float_fun1(float, int); versus Click here to view code image using float_fun2= float (float, int);
In all these examples, the using declaration clearly separates the new type name from the definition.
In addition, the using declaration allows us to define Template Aliases. These are definitions with type parameters. Assume we have a template class for tensors of arbitrary order and parameterizable value type:
Click here to view code image
template <unsigned Order, typename Value> class tensor { … };
Now we like to introduce the type names vector and matrix for tensors of first and second order, respectively. This cannot be achieved with typedef but easily by template aliases via using: Click here to view code image template <typename Value> using vector= tensor<1, Value>; template <typename Value> using matrix= tensor<2, Value>; When we throw the output of the following lines: Click here to view code image std::cout “type of vector<float> is “ typeid(vector<float>).name() ‘\n’; std::cout “type of matrix<float> is “ typeid(matrix<float>).name() ‘\n’; into a name demangler, we will see Click here to view code image type of vector<float> is tensor<1u, float> type of matrix<float> is tensor<2u, float>
Resuming, if you have experience with typedef, you will appreciate the new
opportunities in C++11, and if you are new in the type definition business, you should start with using right away.