C++ Templates
Templates are C++'s mechanism for generic programming — writing code that works with any type without repeating yourself. Unlike runtime polymorphism (virtual functions), templates are resolved entirely at compile time: the compiler generates a separate copy of the function or class for each set of type arguments actually used. This is called template instantiation.
Templates are the foundation of the C++ Standard Library:
std::vector<int>, std::map<std::string, double>,
std::sort(begin, end) — all templates. They power everything from
containers to algorithms to smart pointers. Understanding templates
deeply means understanding how modern C++ achieves its combination of
abstraction and zero-overhead performance.
[[cpp-void-ptr]]
Basic Syntax
A function template parameterizes a function over one or more types (or non-type values).
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// Usage — the compiler deduces T from the argument types
int x = max(3, 5); // T = int
double y = max(3.14, 2.72); // T = double
Class templates parameterize entire classes:
template <typename T>
class Stack {
std::vector<T> data;
public:
void push(const T& val) { data.push_back(val); }
T pop() { T v = data.back(); data.pop_back(); return v; }
bool empty() const { return data.empty(); }
};
Stack<int> intStack;
Stack<std::string> strStack;
Template parameters can also be non-type values — integers, enums, pointers, or (in C++20) floating-point and class types:
template <typename T, size_t N>
class Array {
T data[N];
public:
size_t size() const { return N; }
};
Array<int, 100> buf; // stack-allocated array of 100 ints
Templates in Godot
Godot uses templates heavily throughout its core. A prime example is the
memory allocation layer: Memory::alloc_static<true>(size) uses a
bool template parameter to decide at compile time whether to
zero-fill the allocated memory.
This compile-time dispatch means no runtime branch, no if check, no
function-pointer indirection — the compiler generates two versions,
and the one actually called is resolved during compilation. This pattern
is called static polymorphism and is a key performance technique in
game engines.
Another critical Godot template is Ref<T> (C++ Smart Pointers &
Ref), which is a
reference-counted smart pointer template. It only accepts classes
derived from RefCounted, enforced through template constraints.
Template Specialization — Full and Partial
You can provide a specialized implementation for specific types or families of types. Full specialization replaces the template entirely for one concrete type:
// Primary template
template <typename T>
struct TypeName {
static const char* get() { return "unknown"; }
};
// Full specialization for int
template <>
struct TypeName<int> {
static const char* get() { return "int"; }
};
// Full specialization for double
template <>
struct TypeName<double> {
static const char* get() { return "double"; }
};
Partial specialization specializes for a family of types — for
example, all pointer types, all std::vector<T>, or all types matching
a pattern:
// Partial specialization: matches T* for any T
template <typename T>
struct TypeName<T*> {
static const char* get() { return "pointer"; }
};
// Partial specialization: matches std::vector<T> for any T
template <typename T>
struct TypeName<std::vector<T>> {
static const char* get() { return "vector"; }
};
The compiler always picks the most specialized match. Partial specialization only works for class templates, not function templates (function templates use overloading instead).
SFINAE and enableif
SFINAE stands for "Substitution Failure Is Not An Error" — when the compiler tries to substitute template parameters and the substitution fails (e.g., a type doesn't have a required member), it does not produce an error. Instead, it simply removes that overload from consideration and tries the next one.
This enables conditional template instantiation — including or excluding overloads based on type properties:
// Enable this overload only for integral types
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
divide(T a, T b) {
return a / b; // integer division
}
// Enable this overload only for floating-point types
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
divide(T a, T b) {
return a / b; // floating-point division
}
C++14 simplified this with std::enable_if_t, and C++17 introduced
if constexpr which often replaces SFINAE for simpler cases:
template <typename T>
T divide(T a, T b) {
if constexpr (std::is_integral_v<T>) {
return a / b; // integer path
} else {
return a / b; // floating-point path
}
}
SFINAE is still essential for controlling which template overloads exist
— if constexpr can't remove an overload from the overload set, it
can only branch inside a single instantiation.
Variadic Templates and Parameter Packs
C++11 introduced variadic templates — templates that accept an
arbitrary number of type or value parameters. The syntax uses ...
(ellipsis) to denote a parameter pack:
// Base case — recursion termination
void print() { std::cout << '\n'; }
// Variadic — one or more arguments
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << ' ';
print(rest...); // recursive expansion
}
print(1, 2.5, "hello", 'c'); // prints: 1 2.5 hello c
C++17 introduced fold expressions, which simplify operations over parameter packs:
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // fold: args1 + args2 + ... + argsN
}
auto total = sum(1, 2, 3, 4, 5); // total = 15
Variadic templates power std::tuple, std::variant,
std::make_shared, emplace_back, and nearly every variadic function
in the standard library. They eliminate the pre-C++11 practice of
copy-pasting overloads for 1 arg, 2 args, 3 args, … up to some
arbitrary limit.
C++20 Concepts
C++20 Concepts are a major evolution of templates. They let you specify requirements on template parameters, producing readable error messages when a type doesn't satisfy those requirements — instead of pages of inscrutable template instantiation backtraces.
// Define a concept: T must be comparable with <
template <typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
// Use the concept in place of typename
template <Comparable T>
T max(T a, T b) {
return (a > b) ? a : b;
}
struct NotComparable {};
// max(NotComparable{}, NotComparable{}); // ERROR: NotComparable does not satisfy Comparable
Concepts can also be used with the terser auto syntax:
auto max(Comparable auto a, Comparable auto b) {
return (a > b) ? a : b;
}
Concepts subsume most use cases of SFINAE and enable_if, providing
clearer intent, better error messages, and faster compilation (the
compiler can reject non-matching overloads earlier).
Key Point
Templates execute entirely at compile time. Each unique set of template arguments produces a separate instantiation — separate machine code in the binary. This means:
- Zero runtime overhead — no virtual dispatch, no type erasure wrappers, no indirection. Template code is as fast as hand-written code for each specific type.
- Potentially larger binaries — if you instantiate
std::vector<int>,std::vector<double>, andstd::vector<std::string>, you get three copies ofvector's code. This is called code bloat. In practice, modern linkers can fold identical instantiations (e.g.,vector<int*>andvector<const int*>on some platforms). - Longer compile times — each instantiation requires the compiler to parse, type-check, and codegen the template body for each combination. Large template-heavy codebases (LLVM, Godot) can take minutes or hours to build.
- Error messages at instantiation, not definition — if a
template body contains an error that only surfaces for specific
types (e.g.,
T::valuewhen T isint), the error only appears when you actually instantiate withint. This makes debugging template errors notoriously difficult — a key motivation for C++20 Concepts.
Related Notes
- C++ Smart Pointers & Ref
— Ref<T>, unique
ptr<T>, sharedptr<T> are all class templates - C++ void* Pointers — compile-time type erasure (templates) vs runtime type erasure (void*)
- C++ Function Pointers — std::function is a class template; function pointer types can be template parameters
- C++ Enum vs Struct Class — enums and structs work as template type parameters and non-type template arguments
- C++ Memory Management — custom allocators are templated on the type they allocate
- Copy Constructors — templates interact with copy semantics; template constructors are never copy constructors