Let's introduce our second type of template - variable templates. Let's imagine we were creating a library for our colleagues to use. Our library needs mathematical constants, such as Pi:
constexpr double Pi {
3.141592653589793238462643383279
}
We elected to use a double
here, but that might be presumptuous. We don’t know how our library is going to be used by our consumers, so the preferred type was just a guess. In some scenarios, they might prefer a float, or a custom type.
That conversion could be done at run time, but it would have a performance cost, and may reduce type safety.
We could create different versions of our variable:
constexpr int PiInt {
3
};
constexpr double PiFloat {
3.141592f
};
constexpr double PiDouble {
3.141592653589793
};
But, this has the same disadvantages we saw when trying to create classes to accommodate different data types. Aside from the code duplication and the verbose names, this approach requires us to know all the types our users will want in advance.
Instead, we could create a variable template, using a template parameter in place of the type. In this example, our parameter will be a typename
and we’ll call it T
:
template <typename T>
constexpr T Pi {
3.141592653589793238462643383279
};
Now, we can ask our variable template to create new variables, using the <
and >
syntax. Given that this template requires a type as its parameter, would provide that type as an argument:
template <typename T>
constexpr T Pi{3.141592653589793238462643383279};
int main() {
// Will be an integer initialized to 3
auto IntegerPi{Pi<int>};
}
Any time the compiler encounters the Pi
template being invoked with a type that it hasn't seen, it will use the template to create a new variable, passing the literal 3.141... to that type's constructor.
Given this happens at compile time, the type must have a constructor that can accept that argument at compile time. In the following example, we instantiate Pi<CustomType>
, where CustomType
has a constexpr
constructor that can accept the double
:
#include <iostream>
template <typename T>
constexpr T Pi{3.141592653589793238462643383279};
struct CustomType {
constexpr CustomType(double InitialValue)
: Value{int(InitialValue)} {}
int Value;
};
int main() {
auto Container{Pi<CustomType>};
std::cout << "Container Value: "
<< Container.Value;
}
Container Value: 3.14159
Variable templates are not restricted to just setting the type of the generated variable. Our templates can have multiple parameters, and use them as needed to create a variable.
Below, we define a template that creates integers whose value is initialized to the result of calling the +
operator on two integer parameters:
template <int x, int y>
constexpr int Result{x + y};
int main() {
int MySum{Result<1, 2>};
}
Given this calculation is done at compile time, we can ensure there is no runtime performance impact which is ideal if our calculation is expensive, or performed frequently.
However, it also means the expressions we use in our template variable must be evaluable at compile time. In the previous example, operator+(int, int)
meets this criterion, so our template is valid.
constexpr
with Template VariablesIn almost all scenarios, we will want to mark template variables as constexpr
, preventing them from being modified. Similar to class templates, variable templates deduplicate themselves based on their arguments.
For example, if we have multiple invocations of Result<1, 2>
, only a single variable will be generated. All references to Result<1, 2>
will share that same variable. Therefore, if one of those locations modifies the variable, it can create unexpected behaviour elsewhere in our code:
#include <iostream>
template <int x, int y>
int Result{x + y}; // not const
int main() {
++Result<1, 2>;
// ...
int MySum{Result<1, 2>};
std::cout << "MySum: " << MySum;
}
MySum: 4
Marking our variable const
(or constexpr
) prevents this from happening:
template <int x, int y>
constexpr int Result{x + y};
int main() {
++Result<1, 2>;
}
error: increment of read-only variable 'Result<1, 2>'
It does not prevent variables created from our template from being modified A non-const variable like MySum
can be created from a const
template. Because MySum
is not a reference, it is created by copying the value Result<1, 2>
.
Therefore, modifications to MySum
are not modifying Result<1, 2>
- they’re acting on a copy:
#include <iostream>
template <int x, int y>
constexpr int Result{x + y};
int main() {
int MySum{Result<1, 2>};
std::cout << "MySum is now: " << ++MySum;
std::cout << "\nResult<1, 2> is still: "
<< Result<1, 2>;
}
MySum is now: 4
Result<1, 2> is still: 3
consteval
Functions (C++20)Since the introduction of consteval
in C++20, we now have an additional way to implement compile-time calculations. Our previous variable template could be replaced with this function, if preferred:
consteval int Result(int x, int y) {
return x + y;
}
int main() {
int MySum{Result(1, 2)};
}
This is more suitable for complex use cases, as it gives us the full flexibility of a function body. That can include if
statements, local variables, and more to determine the resulting value.
It won’t work if our parameter list needs to include a typename
, but we’ll see how we can solve that later in the chapter when we introduce function templates.
In this lesson, we learned about variable templates in C++. Here are the key takeaways:
constexpr
to prevent unexpected behavior due to shared instances.constexpr
constructors.An introduction to variable templates, allowing us to create variables at compile time.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.