Variable Templates
An introduction to variable templates, allowing us to create variables at compile time.
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.
Creating Variable Templates
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;Note we're using copy assignment (=) here to allow narrowing casts. We can alternatively use static_cast to explicitly convert the value to the requested type T if preferred:
template <typename T>
constexpr T Pi{static_cast<T>(
3.141592653589793238462643383279
)};Either way, we can now ask our variable template to create new variables, using the < and > syntax. Given that this template requires a type as its parameter, we would provide that type as an argument:
template <typename T>
constexpr T Pi = 3.141592653589793238462643383279;
int main() {
// This will be an integer initialized to 3
int IntegerPi{Pi<int>};
}Variable Templates with Custom Types
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.
As with all templates, this happens at compile time, so 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.14159Non-Typename Parameters
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.
Using constexpr with Template Variables
In 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: 4Marking 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 in the following example can be created from a const template. In this case, 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: 3Using consteval Functions (C++20)
Since the introduction of consteval in C++20, we now have an additional way to enforce compile-time calculations. Our previous variable template could be replaced with this function:
consteval int Result(int x, int y) {
return x + y;
}
int main() {
int MySum{Result(1, 2)};
}This technique 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.
Summary
In this lesson, we learned about variable templates in C++. Here are the key takeaways:
- Variable templates allow us to create variables with parameterized types, similar to class templates.
- They provide flexibility and reusability by allowing the user to specify the desired type when instantiating the variable.
- Variable templates can have multiple parameters and perform compile-time calculations.
- It's recommended to mark variable templates as
constexprto prevent unexpected behavior due to shared instances. - Variable templates offer an alternative to creating multiple versions of variables for different types.
- They enable us to create variables with custom types that have
constexprconstructors. - Variable templates can be used for compile-time calculations, ensuring no runtime performance impact.
Function Templates
Understand the fundamentals of C++ function templates and harness generics for more modular, adaptable code.