Variable Templates (C++14)
Variable templates parameterize a variable by type or value — powering _v trait shortcuts, mathematical constants, and type-safe compile-time lookups.
Variable Templatesince C++14A variable template declares a family of variables parameterized by type or non-type arguments, allowing the same name to produce distinct, independently typed or valued constants depending on the template arguments supplied at instantiation.
Overview
Variable templates close the gap between type-level computation and value access. Before C++14, querying a type trait required spelling out std::is_integral<T>::value at every call site — member access on a struct specialization. Variable templates made std::is_integral_v<T> possible: a direct variable lookup with no extra struct template machinery at the call site.
Their utility extends well beyond convenience aliases. Variable templates are the right mechanism whenever a constant's type, value, or both depend on a template argument: floating-point mathematical constants that preserve per-type precision, physics constants for numeric simulation libraries, per-type compile-time property tables, and tag sentinels that vary by policy type.
Since C++17, inline constexpr is the canonical form for any variable template placed in a header. The inline specifier instructs the linker to merge multiple identical definitions across translation units — without it, the same specialization included in several .cpp files silently violates the One Definition Rule.
Syntax
// Type parameter — value type matches T
template<typename T>
constexpr T zero = T{}; // C++14
// Non-type parameter
template<int N>
constexpr int square = N * N; // C++14
// Mixed: type + non-type
template<typename T, std::size_t N>
constexpr T nth_power_of_two = T(1) << N; // C++14
// Inline — ODR-safe for headers
template<typename T>
inline constexpr bool is_integral_v = // C++17
std::is_integral<T>::value;
// Constrained parameter
template<typename T>
requires std::floating_point<T> // C++20
inline constexpr T tau = T(6.283185307179586476925286766559L);Partial and full specialization work exactly as they do for class templates:
template<typename T>
inline constexpr bool is_std_array = false; // primary, C++17
template<typename T, std::size_t N>
inline constexpr bool is_std_array<std::array<T, N>> = true; // partial specializationExamples
The _v pattern: type trait shortcuts
Every _v variable in <type_traits> (C++17) follows the same pattern — a variable template wrapping a ::value member:
// How the standard library defines them (representative examples)
template<typename T>
inline constexpr bool is_trivially_copyable_v = // C++17
std::is_trivially_copyable<T>::value;
template<typename T, typename U>
inline constexpr bool is_same_v = std::is_same<T, U>::value; // C++17Defining your own follows the same idiom. Write the trait struct first, then its _v alias immediately alongside:
template<typename T>
struct is_smart_pointer : std::false_type {};
template<typename T>
struct is_smart_pointer<std::unique_ptr<T>> : std::true_type {};
template<typename T>
struct is_smart_pointer<std::shared_ptr<T>> : std::true_type {};
template<typename T>
struct is_smart_pointer<std::weak_ptr<T>> : std::true_type {};
template<typename T>
inline constexpr bool is_smart_pointer_v = // C++17
is_smart_pointer<T>::value;
static_assert(is_smart_pointer_v<std::unique_ptr<int>>);
static_assert(!is_smart_pointer_v<int*>);Mathematical constants
Before C++20's <numbers>, variable templates were the canonical mechanism for type-safe, full-precision math constants:
template<typename T>
constexpr T pi = // C++14
T(3.141592653589793238462643383279502884L);
template<typename T>
constexpr T e = // C++14
T(2.718281828459045235360287471352662498L);
// The L suffix preserves long double precision before narrowing to T.
// Without it, the double literal loses bits prior to conversion.
float area_f = pi<float> * r * r;
double area_d = pi<double> * r * r;
long double area_ld = pi<long double> * r * r;C++20 standardized this via <numbers>, which uses the identical pattern:
#include <numbers> // C++20
double d = std::numbers::pi; // double by default
float f = std::numbers::pi_v<float>;
long double ld = std::numbers::e_v<long double>;
// Also: log2e, log10e, ln2, ln10, sqrt2, sqrt3, phi, egamma, inv_pi, inv_sqrtpi, inv_sqrt3Constrained variable templates (C++20)
Concept constraints on variable templates produce clear diagnostics at the point of instantiation, rather than deep inside constexpr evaluation:
#include <concepts>
template<std::floating_point T> // C++20
inline constexpr T inv_pi = T(1) / std::numbers::pi_v<T>;
// inv_pi<int>; // error: constraint not satisfied — at instantiation site
// inv_pi<float>; // okNon-type parameters can be constrained too:
template<std::size_t N>
requires (N > 0 && (N & (N - 1)) == 0) // C++20 — N must be a power of two
inline constexpr std::size_t log2_pow2 = __builtin_ctzll(N);
static_assert(log2_pow2<8> == 3);
static_assert(log2_pow2<1024> == 10);Partial specialization for compile-time property tables
template<typename T>
inline constexpr std::size_t element_count = 1; // C++17
template<typename T, std::size_t N>
inline constexpr std::size_t element_count<std::array<T, N>> = N;
template<typename A, typename B>
inline constexpr std::size_t element_count<std::pair<A, B>> = 2;
template<typename... Ts>
inline constexpr std::size_t element_count<std::tuple<Ts...>> = sizeof...(Ts);
static_assert(element_count<std::array<int, 7>> == 7);
static_assert(element_count<std::tuple<int, double, char>> == 3);
static_assert(element_count<int> == 1);This pattern composes well with SFINAE or concepts-based dispatch — the table is pure data, no functions involved.
Domain-specific constants in generic code
namespace si {
template<typename T>
inline constexpr T speed_of_light = T(299'792'458); // m/s, exact (SI 2019)
template<typename T>
inline constexpr T planck = T(6.62607015e-34); // J·s, exact (SI 2019)
template<typename T>
inline constexpr T boltzmann = T(1.380649e-23); // J/K, exact (SI 2019)
} // namespace si
template<typename T>
T thermal_de_broglie(T mass, T temperature) {
using std::sqrt;
return si::planck<T> /
sqrt(T(2) * mass * si::boltzmann<T> * temperature);
}Best Practices
Always use inline constexpr in headers (C++17+). Variable templates without inline silently violate ODR when included in multiple translation units. The inline qualifier allows the linker to merge identical specializations; without it the behavior is undefined, with no diagnostic required by the standard.
Write the _v alias immediately alongside its trait struct. Callers that use the variable form are cleaner and do not instantiate an extra struct specialization at the call site. Make it a habit: trait struct and variable alias live in the same header, adjacent.
Use long double literals for floating-point constants. Write T(3.14159...L) not T(3.14159...). The L suffix keeps the source constant at full long double precision before narrowing; a bare double literal silently loses the low bits that differentiate double from long double.
Prefer <numbers> in C++20 codebases. std::numbers::pi_v<T> is tested, portable, and documented. Re-defining pi in each project is unnecessary in C++20.
Constrain type parameters when the domain is restricted. A requires std::floating_point<T> guard gives a clear compile error at the template; without it, the error surfaces deep inside whatever expression uses the constant.
Common Pitfalls
Missing inline is an ODR trap (C++14 only in headers). The compiler will not warn. The linker silently picks one definition. The symptom is inconsistent constant values across TUs or subtle link-time behavior. Upgrade all header-resident variable templates to inline constexpr the moment you target C++17.
// ❌ ODR violation if included in multiple TUs
template<typename T>
constexpr T pi = T(3.14159265358979L); // C++14, dangerous in headers
// ✓ safe
template<typename T>
inline constexpr T pi = T(3.14159265358979L); // C++17Specializing after first use is ill-formed. The specialization must appear before any instantiation of that specialization within the same translation unit. Placing specializations in separate optional headers that users may or may not include is a latent violation — keep all specializations of a template in one authoritative header.
Unconstrained variable templates give poor diagnostics. A variable template instantiated with an incompatible type fails at the point of instantiation with an error rooted inside the initializer expression. A requires clause (C++20) or a static_assert in the initializer shifts the error to the template boundary with a legible message.
// Poor diagnostic — error inside numeric_limits internals
template<typename T>
inline constexpr T max_val = std::numeric_limits<T>::max();
// Clear diagnostic at instantiation site
template<typename T>
requires std::is_arithmetic_v<T> // C++20
inline constexpr T max_val = std::numeric_limits<T>::max();Partial specializations must reside in the same namespace as the primary. Attempting to specialize inside a nested namespace or an unrelated scope creates a new, unrelated primary template — the compiler may not warn, and the wrong template is silently selected.
Standard Library _v Helpers (C++17)
Every _v name in <type_traits> is a variable template wrapping a ::value member access. The pattern is entirely mechanical: a bool variable template whose initializer dereferences the corresponding struct template's ::value. C++17 added the complete set so callers never need to write the struct form at call sites.
// How the standard library defines the _v helpers — C++17
namespace std {
template<typename T>
inline constexpr bool is_integral_v = // C++17
is_integral<T>::value;
template<typename T, typename U>
inline constexpr bool is_same_v = // C++17
is_same<T, U>::value;
template<typename From, typename To>
inline constexpr bool is_convertible_v = // C++17
is_convertible<From, To>::value;
template<typename Base, typename Derived>
inline constexpr bool is_base_of_v = // C++17
is_base_of<Base, Derived>::value;
template<typename T>
inline constexpr bool is_nothrow_move_constructible_v = // C++17
is_nothrow_move_constructible<T>::value;
} // namespace stdThe mechanical nature means you can apply the same pattern to any custom trait. The _v alias costs nothing at runtime — the struct specialization still exists and is used for the ::value lookup; the variable template simply names the result.
// Call-site comparison
// C++11/14 form — struct member access
static_assert(std::is_integral<int>::value);
static_assert(std::is_same<int, int>::value);
// C++17 form — direct variable lookup, identical semantics
static_assert(std::is_integral_v<int>); // C++17
static_assert(std::is_same_v<int, int>); // C++17
static_assert(std::is_convertible_v<float, double>); // C++17
static_assert(std::is_base_of_v<std::ios_base, std::ostream>); // C++17constexpr Variable Templates for Math Constants
A type-parameterized constant lets each floating-point type receive the precision it can actually represent. Storing pi as a double literal and casting it to float discards no information float could have used, but storing it as a long double literal before narrowing to T ensures every type gets the best available approximation.
template<typename T>
inline constexpr T pi = // C++14
T(3.14159265358979323846264338327950288L);
template<typename T>
inline constexpr T e = // C++14
T(2.71828182845904523536028747135266250L);
template<typename T>
inline constexpr T sqrt2 = // C++14
T(1.41421356237309504880168872420969808L);
// Each specialization carries only the bits T can hold
static_assert(sizeof(pi<float>) == 4);
static_assert(sizeof(pi<double>) == 8);
static_assert(sizeof(pi<long double>) == sizeof(long double));
float area_f = pi<float> * r * r;
double area_d = pi<double> * r * r;C++20 standardized exactly this pattern in <numbers>, using the _v suffix to expose the type-parameterized form alongside a double default:
#include <numbers> // C++20
// std::numbers::pi — double (default)
// std::numbers::pi_v<T> — variable template, same as above pattern
double d = std::numbers::pi; // C++20
float f = std::numbers::pi_v<float>; // C++20
long double ld = std::numbers::e_v<long double>; // C++20For C++14/17 codebases, roll your own; for C++20+, prefer <numbers> because it is tested and documents intent.
Variable Templates as Type-Dispatching Tables
A variable template with partial specializations acts as a pure-data lookup table indexed by type. No function call overhead, no if-chains, and no ADL complications — the compiler selects the right specialization at instantiation time.
// Primary — will produce a compile error if used with an unsupported type
template<typename T>
inline constexpr auto format_spec = "%d"; // C++14 — default: integer
template<>
inline constexpr auto format_spec<double> = "%f";
template<>
inline constexpr auto format_spec<float> = "%f";
template<>
inline constexpr auto format_spec<char*> = "%s";
template<>
inline constexpr auto format_spec<const char*> = "%s";
// Use in a thin printf-style wrapper
template<typename T>
void print_value(T value) {
std::printf(format_spec<T>, value);
}
print_value(42); // uses "%d"
print_value(3.14); // uses "%f"
print_value("hello"); // uses "%s"The same idiom works for std::string_view type name tables, eliminating the need for RTTI or typeid:
template<typename T>
inline constexpr std::string_view type_name = "unknown"; // C++14
template<> inline constexpr std::string_view type_name<int> = "int";
template<> inline constexpr std::string_view type_name<float> = "float";
template<> inline constexpr std::string_view type_name<double> = "double";
template<> inline constexpr std::string_view type_name<bool> = "bool";
// type_name<int> == "int" at compile time
// type_name<float> == "float" at compile timeBecause std::string_view is a trivially constructible literal type, the table has zero runtime overhead.
Combining Variable Templates with Concepts (C++20)
Concept constraints on variable templates reject invalid instantiations at the template boundary with a readable error message. Without a constraint, the error propagates into the initializer expression — often inside <limits> or another library header — making the diagnostic unhelpful.
#include <concepts>
#include <limits>
template<std::integral T> // C++20
inline constexpr T max_value = std::numeric_limits<T>::max();
template<std::integral T> // C++20
inline constexpr T min_value = std::numeric_limits<T>::min();
static_assert(max_value<int> == INT_MAX);
static_assert(max_value<uint8_t> == 255);
// max_value<float>; // error: 'float' does not satisfy 'std::integral'The requires clause form is equivalent and allows compound predicates that concept names cannot express as a single identifier:
template<typename T>
requires std::floating_point<T> // C++20
inline constexpr T epsilon = std::numeric_limits<T>::epsilon();
template<typename T>
requires std::floating_point<T> // C++20
inline constexpr T infinity = std::numeric_limits<T>::infinity();
// Use in numeric code without spelling out numeric_limits everywhere
if (std::abs(a - b) < epsilon<double>) { /* equal */ }Concepts on variable templates compose: a variable template that depends on another constrained template inherits the constraints naturally through the initializer expression.
Variable Templates in SFINAE and enable_if
Variable templates made the _v trait helpers that SFINAE idioms depend on far more readable. Pre-C++17 SFINAE required std::enable_if<std::is_integral<T>::value> at every overload boundary; with the _v aliases, the same expression becomes std::enable_if<std::is_integral_v<T>>.
// C++14: SFINAE with enable_if, before _v aliases existed
template<typename T,
typename = std::enable_if_t<std::is_integral<T>::value>> // C++14
T safe_shift(T value, int bits) { return value << bits; }
// C++17: cleaner with _v helpers
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>> // C++17
T safe_shift(T value, int bits) { return value << bits; }
// C++17: cleanest — if constexpr avoids SFINAE entirely
template<typename T>
T safe_shift(T value, int bits) {
if constexpr (std::is_integral_v<T>) // C++17
return value << bits;
else
static_assert(sizeof(T) == 0, "safe_shift requires an integral type");
}Variable templates also enable elegant pack-size lookups without a helper struct:
// Count the types in a TypeList without a member struct
template<typename>
struct TypeList {};
template<typename List>
inline constexpr std::size_t type_list_size_v = 0; // C++14 — primary
template<typename... Ts>
inline constexpr std::size_t // C++14 — partial specialization
type_list_size_v<TypeList<Ts...>> = sizeof...(Ts);
static_assert(type_list_size_v<TypeList<int, double, char>> == 3);
static_assert(type_list_size_v<TypeList<>> == 0);The specialization pattern replaces what would otherwise require a member value in a helper struct, keeping the call site free of ::value dereference noise.
Variable Templates vs constexpr Functions
Variable templates, constexpr functions, and constexpr class static members each occupy a distinct niche. The right choice depends on whether computation is needed, whether partial specialization matters, and how callers will use the result.
| Criterion | Variable template | constexpr function | constexpr static member |
|---|---|---|---|
| Use-site syntax | pi<double> | pi<double>() | Constants<double>::pi |
| Runtime arguments | No | Yes | No |
| Partial specialization | Yes | No | Yes (via class template) |
| ODR-safe in headers | Yes (inline) | Yes (always) | Requires inline (C++17) |
| ADL participation | No | Yes | No |
| Works as NTTP | Yes | Yes (C++20 only) | Yes |
Variable templates win when the value is a type-indexed constant with no computation — the _v trait helpers, math constants, and type-property tables all fall here. constexpr functions win when the result depends on runtime or compile-time arguments beyond the type: clamping, rounding, or iterative algorithms. Class static members are appropriate when the constant is logically owned by a type and benefits from encapsulation alongside other members of that class.
// Prefer variable template: pure type-indexed constant
template<typename T>
inline constexpr T pi = T(3.14159265358979323846L); // C++14
// Prefer constexpr function: computation over a value
template<typename T>
constexpr T circle_area(T radius) { // C++14
return pi<T> * radius * radius;
}
// Prefer static member: constant belongs to a policy/traits class
template<typename T>
struct NumericPolicy {
static inline constexpr T tolerance = // C++17
std::numeric_limits<T>::epsilon() * T(100);
};Mixing the forms is idiomatic: a constexpr function that references a variable template constant is both expressive and zero-overhead.
See Also
- Type Traits — the full
<type_traits>catalog and_valias list - Concepts —
requiresconstraints on template parameters (C++20) - constexpr — compile-time evaluation rules
- Alias Templates —
template<typename T> usingfor type-level aliases <numbers>— standardized mathematical constants (C++20)