Skip to content
C++
Language
since C++14
Basic

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++14

A 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

cpp
// 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:

cpp
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 specialization

Examples

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:

cpp
// 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++17

Defining your own follows the same idiom. Write the trait struct first, then its _v alias immediately alongside:

cpp
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:

cpp
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:

cpp
#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_sqrt3

Constrained variable templates (C++20)

Concept constraints on variable templates produce clear diagnostics at the point of instantiation, rather than deep inside constexpr evaluation:

cpp
#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>;  // ok

Non-type parameters can be constrained too:

cpp
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

cpp
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

cpp
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.

cpp
// ❌ 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++17

Specializing 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.

cpp
// 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.

cpp
// 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 std

The 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.

cpp
// 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++17

constexpr 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.

cpp
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:

cpp
#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++20

For 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.

cpp
// 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:

cpp
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 time

Because 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.

cpp
#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:

cpp
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>>.

cpp
// 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:

cpp
// 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.

CriterionVariable templateconstexpr functionconstexpr static member
Use-site syntaxpi<double>pi<double>()Constants<double>::pi
Runtime argumentsNoYesNo
Partial specializationYesNoYes (via class template)
ODR-safe in headersYes (inline)Yes (always)Requires inline (C++17)
ADL participationNoYesNo
Works as NTTPYesYes (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.

cpp
// 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 _v alias list
  • Concepts — requires constraints on template parameters (C++20)
  • constexpr — compile-time evaluation rules
  • Alias Templates — template<typename T> using for type-level aliases
  • <numbers> — standardized mathematical constants (C++20)