Summary
Templates are C++‘s mechanism for writing code that works with any type, parameterised at compile time. A template<typename T> class or function generates separate, fully optimised machine code for each type it is instantiated with — no runtime overhead, but the types must be known at compile time. C++20 adds concepts to constrain template arguments with meaningful compile-time error messages. Lambdas and function objects are the primary practical tools built on the template mechanism.
(Stroustrup, A Tour of C++, 3rd ed., Chs. 7–8, see source-a-tour-of-cpp)
Class templates
A class template parameterises a class with one or more type arguments:
template<typename T>
class Vector {
public:
explicit Vector(int s);
~Vector() { delete[] elem; }
T& operator[](int i);
const T& operator[](int i) const;
int size() const { return sz; }
private:
T* elem;
int sz;
};Instantiate with a concrete type:
Vector<char> vc(200); // vector of chars
Vector<string> vs(17); // vector of strings
Vector<list<int>> vli(45); // vector of lists of intsEach instantiation is a separate concrete class. Vector<double> and Vector<int> are distinct types with no runtime relationship.
template<class T> vs template<typename T>: these are synonyms. typename is more common in modern code.
Function templates
A function template is parameterised by type and the type is deduced from the arguments:
template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v)
{
for (auto x : s)
v += x;
return v;
}
// Usage — types deduced automatically:
int total = sum(vec_of_ints, 0);
double avg = sum(vec_of_ints, 0.0);No need to specify template arguments explicitly when they can be deduced. The compiler generates a concrete instantiation for each combination of types used.
Concepts (C++20)
Without constraints, template errors appear at instantiation time — deep in the compiler’s output, often unreadable. A concept is a compile-time predicate that constrains which types a template accepts:
template<Element T> // Element is a concept
class Vector { /* ... */ };If a type fails the concept check, the compiler reports an error at the point of use with a human-readable message.
Standard library concepts live in <concepts>. Examples:
std::copyable<T>— T can be copiedstd::integral<T>— T is an integral typestd::floating_point<T>— T is a floating-point typestd::invocable<F, Args...>— F is callable with Args
template<std::integral T>
T twice(T x) { return x + x; }
twice(3); // OK
twice(3.14); // ERROR: 3.14 is not integral — clear messageC++20 compatibility: Unreal Engine 5 supports C++17 and partial C++20. Concepts require C++20 and may not be available in all project configurations.
Value template arguments
Templates can also take non-type (value) arguments:
template<typename T, int N>
struct Buffer {
T elem[N];
constexpr int size() { return N; }
};
Buffer<char, 1024> buf; // statically-sized buffer, no heap allocationValue arguments must be constant expressions — known at compile time. This is used for fixed-size containers, compile-time configuration, and policy objects.
Template argument deduction (CTAD)
In C++17+, the compiler can deduce class template arguments from constructor arguments:
pair<int, double> p1 = {1, 5.2}; // explicit
pair p2 = {1, 5.2}; // CTAD: deduces pair<int, double>
Vector v {1, 2, 3}; // deduces Vector<int>CTAD removes redundancy in many common cases. Be aware that deduction is not always obvious — Vector vs {"Hello", "World"} deduces Vector<const char*>, not Vector<string>.
Function objects (functors)
A function object (functor) is an object of a class that defines operator(). It can carry state (unlike a plain function) and is callable like a function:
template<typename T>
class Less_than {
const T val;
public:
Less_than(const T& v) : val{v} {}
bool operator()(const T& x) const { return x < val; }
};
Less_than<int> lt42 {42};
bool b = lt42(37); // true: 37 < 42Function objects are the mechanism behind STL algorithms:
sort(v.begin(), v.end(), Less_than<int>{10}); // sort by: x < 10They are more efficient than function pointers because operator() can be inlined.
Lambda expressions
Lambda expressions provide a concise syntax for creating anonymous function objects:
auto is_small = [](int x) { return x < 10; };
bool b = is_small(7); // trueCapture lists specify how the lambda accesses variables from the enclosing scope:
| Capture | Meaning |
|---|---|
[] | Capture nothing |
[&] | Capture all by reference |
[=] | Capture all by value (copy) |
[x] | Capture only x by value |
[&x] | Capture only x by reference |
[this] | Capture this (current object, by reference) |
[*this] | Capture copy of current object |
int threshold = 42;
// Capture by reference — lambda can see changes to threshold
auto below_ref = [&](int x) { return x < threshold; };
// Capture by value — lambda has its own copy of threshold at time of creation
auto below_val = [=](int x) { return x < threshold; };Generic lambda (auto parameter): accepted as any type, effectively a template lambda:
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int
add(1.5, 2.5); // doubleLambdas in practice — the most common use is as arguments to standard algorithms:
vector<int> v {5, 1, 3, 2, 4};
sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
auto count_big = count_if(v.begin(), v.end(), [](int x) { return x > 3; });Templates vs C# generics
| C++ Templates | C# Generics | |
|---|---|---|
| When resolved | Compile time | Compile time for value types; runtime for reference types |
| Code generated | Separate code for each instantiation | Shared code for reference types (type erasure) |
| Runtime overhead | Zero | Negligible for value types; minimal boxing for reference types |
| Error messages (unconstrained) | Poor (deep in instantiation) | Moderate |
| Error messages (constrained) | Good (concepts, C++20) | Good (interfaces/where constraints) |
| Template metaprogramming | Extensive — can compute at compile time | Limited (primarily runtime) |
| Default function visibility | Not virtual | Depends on base class |
The practical implication: C++ templates generate a binary footprint proportional to the number of distinct type arguments. Ten instantiations of a large template class produce ten copies of the generated code. C# generics for reference types share one implementation.
In practice (Unreal Engine context)
Unreal Engine uses templates extensively:
TArray<T>,TMap<K,V>,TSet<T>— Unreal’s container types (analogues tovector,map,set)TSharedPtr<T>,TWeakPtr<T>— Unreal’s smart pointer equivalentsTSubclassOf<T>— type-safe class reference for BlueprintTOptional<T>— optional value (similar tostd::optional)- Delegate templates:
DECLARE_DYNAMIC_DELEGATE,DECLARE_DELEGATE_RetValmacros generate template-based callback types
Unreal’s templates follow the same principles as std templates, but are adapted for Unreal’s reflection system, UPROPERTY compatibility, and historical platform constraints.
Open questions
- C++ template errors (without concepts) are notoriously hard to read. What diagnostic strategies help in practice — static assertions, type traits, or relying on IDE error highlighting?
- Unreal Engine has its own reflection system that limits some template uses (e.g., UPROPERTY cannot be a template member). Where do these restrictions most commonly catch developers?
Related
- cpp-basics —
autotype deduction; range-for (iterator protocol underlying templates) - cpp-classes-and-oop — class hierarchy vs templates for polymorphism trade-offs
- cpp-memory-management —
unique_ptr<T>andshared_ptr<T>as canonical template types - csharp-collections — C#
List<T>/Dictionary<K,V>comparison - source-a-tour-of-cpp