Generic Programming, Templates and the Standard LibraryIntroduction and ContextObject-oriented programming organizes software around objects, classes, encapsulation, inheritance, and polymorphism. These mechanisms are powerful, but many algorithms and data structures do not depend on a particular object type. A sorting algorithm, stack, search routine, or numerical transformation often has the same logic for integers, strings, user-defined classes, and domain-specific types. Generic programming addresses this commonality by expressing algorithms and data structures in terms of requirements rather than concrete types. In C++, generic programming is implemented primarily through templates and supported extensively by the Standard Library. Templates enable compile-time parameterization; the Standard Library supplies reusable containers, iterators, algorithms, function objects, utilities, ranges, and concurrency facilities. Together they form one of the most important sources of abstraction and productivity in modern C++. Generic programming is not simply “writing one function that accepts many types.” It is a design approach based on: - identifying the operations an algorithm requires;
- expressing those requirements through interfaces and concepts;
- separating algorithms from data representation;
- resolving as much as possible at compile time; and
- preserving type safety without unnecessary runtime overhead.
At postgraduate level, it is important to distinguish language mechanisms from library conventions. Templates are a language feature, while containers, iterators, algorithms, and concepts are library and design abstractions built around that feature. Learning ObjectivesAfter completing this chapter, the learner should be able to: - Explain the motivation, principles, and trade-offs of generic programming.
- Distinguish function templates, class templates, variable templates, aliases, and non-type template parameters.
- Analyze template deduction, specialization, overload resolution, and constraints.
- Apply standard containers, iterators, algorithms, ranges, and utilities appropriately.
- Evaluate complexity, ownership, exception safety, compile-time cost, and binary-interface implications.
- Design reusable generic components using modern C++ practices.
- Critically assess limitations and risks of template-heavy systems.
1. Generic Programming as a Design ParadigmAn object-oriented interface commonly defines a stable runtime contract. Generic programming defines a largely compile-time contract: a type is acceptable if it supports the operations required by the algorithm. This is often called structural or implicit polymorphism. For example, a function that adds two values requires an addition operation, but not inheritance from an Addable base class. A sorting algorithm requires comparison and movement or swapping, but it need not know whether the elements are integers or records. Generic programming differs from traditional runtime polymorphism in several respects: | Dimension | Runtime polymorphism | Generic programming |
|---|
| Binding time | Usually runtime | Usually compile time | | Mechanism | Virtual functions and inheritance | Templates and constraints | | Type relationship | Explicit common interface | Required operations and concepts | | Typical overhead | Possible indirection | Often optimized away | | Flexibility | Objects through a base interface | Any type meeting requirements | | Main cost | Runtime dispatch and object design | Compilation time and diagnostics |
Neither paradigm universally dominates. Runtime polymorphism is useful when types are discovered dynamically or must cross stable binary boundaries. Generic programming is especially effective when types are known during compilation and performance is important. 2. Function TemplatesA function template describes a family of functions. The compiler creates a specialization when the template is used with particular template arguments. template <typename T>
T maximum(T a, T b) {
return (a < b) ? b : a;
}The identifier T is a type template parameter. The expression a < b imposes an implicit requirement: T must support an ordering operation compatible with the expression. Calls such as maximum(4, 7) and maximum(2.5, 1.4) cause different specializations to be generated. Template argument deductionWhen a template argument is not written explicitly, the compiler attempts to deduce it from function arguments. Deduction is affected by references, pointers, arrays, cv-qualifiers, and conversions. template <typename T>
void inspect(T value);
template <typename T>
void inspect_ref(const T& value); For inspect(5), T is deduced as int. For inspect_ref(5), T is also generally deduced as int, while the function parameter remains const int&. Reference parameters often preserve more information than pass-by-value parameters, particularly for arrays and string literals. Explicit template arguments are useful when deduction is impossible or when the programmer wants to control the selected type: auto result = maximum<double>(3, 4.5); Here the arguments are converted to double for the selected specialization. Multiple parameters and forwardingTemplates can accept different types: template <typename L, typename R>
auto multiply(L left, R right) {
return left * right;
}For generic interfaces that must preserve lvalue/rvalue category, forwarding references and std::forward are used: template <typename T>
void relay(T&& value) {
consume(std::forward<T>(value));
}This technique is powerful but should be used only when preserving value category is part of the design. Otherwise, a simple const T& or value parameter is usually clearer. 3. Class Templates and Generic Data StructuresA class template parameterizes the representation or behavior of a class. template <typename T, std::size_t Capacity>
class FixedBuffer {
std::array<T, Capacity> data_{};
std::size_t size_ = 0;
public:
void push_back(const T& value) {
if (size_ == Capacity) throw std::overflow_error("buffer full");
data_[size_++] = value;
}
std::size_t size() const noexcept { return size_; }
const T& operator[](std::size_t i) const { return data_[i]; }
};T is a type parameter and Capacity is a non-type template parameter. The capacity is known at compile time, allowing storage to be embedded directly in the object. A class template may define constructors, member templates, nested types, operators, and constraints.
The standard library uses this approach extensively. std::vector<T>, std::array<T, N>, std::optional<T>, and std::unique_ptr<T> are all class templates with different ownership, storage, and lifetime semantics. Template aliasesAn alias template creates a convenient name without defining a new type: template <typename T>
using MatrixRow = std::vector<T>; An alias is useful for policy combinations, allocator types, and complex dependent types. It does not create a distinct nominal type. Variable templatesA variable template parameterizes a variable: template <typename T>
inline constexpr bool is_pointer_v = std::is_pointer_v<T>; The _v convention is common in the type-traits library and improves readability compared with accessing a nested ::value member. 4. Specialization and OverloadingA primary template provides the general implementation. Explicit specialization supplies a different implementation for a particular type or value. template <typename T>
struct Formatter {
static std::string format(const T& value) {
return std::to_string(value);
}
};
template <>
struct Formatter<bool> {
static std::string format(bool value) {
return value ? "true" : "false";
}
};Full specialization is appropriate when a type has genuinely different semantics. It should not be used merely to compensate for a poorly designed general interface. Partial specialization applies to a family of types and is available for class templates, but not function templates. For function templates, overloads or constrained overloads are normally preferred. Specialization can create maintenance difficulties because behavior becomes distributed across primary templates, specializations, overloads, and namespace rules. A specialization should be documented as part of the generic component’s contract. 5. Concepts and ConstraintsEarlier C++ standards allowed templates to be instantiated until an invalid expression produced a lengthy diagnostic. Modern C++ supports concepts, named compile-time predicates that express template requirements directly. template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T>
T average(T a, T b) {
return (a + b) / 2;
}A requires clause provides another notation: template <typename T>
requires requires(T x) { x.begin(); x.end(); }
void process(const T& container);Concepts improve: - interface documentation;
- overload selection;
- compiler diagnostics;
- prevention of accidental acceptance of unsuitable types; and
- reasoning about generic contracts.
A concept is not merely a syntactic filter. It should express semantically meaningful requirements. For example, std::sortable involves more than the existence of an operator; it represents a collection of requirements concerning ordering, movement, and iterator behavior. Compile-time versus runtime polymorphismA constrained template still produces code for concrete types. A virtual interface instead permits one implementation to operate on objects whose dynamic types are discovered at runtime. Generic code therefore tends to provide specialization and optimization, while runtime polymorphism tends to provide substitution and binary stability. 6. The Standard Library ArchitectureThe Standard Library is best understood through interacting abstractions rather than isolated classes. ContainersContainers manage collections of objects. Their selection should follow access, insertion, ownership, and locality requirements. | Container | Strength | Important limitation |
|---|
std::vector | Contiguous storage, fast indexing, excellent locality | Insertion can invalidate iterators and move elements | std::array | Fixed-size contiguous storage, no dynamic allocation | Size fixed at compile time | std::deque | Efficient insertion at both ends | Not fully contiguous | std::list | Stable node references and constant-time splicing | Poor locality and high per-node overhead | std::forward_list | Low-overhead singly linked structure | Forward traversal only | std::map | Ordered key-value operations | Typically logarithmic operations and node overhead | std::unordered_map | Average constant-time lookup | Requires hashing; worst-case degradation possible | std::set | Ordered unique keys | No direct indexing |
The phrase “constant time” describes an asymptotic bound, not a universal performance guarantee. A vector’s contiguous memory can outperform a linked list even when both offer an operation with similar asymptotic classification. IteratorsIterators generalize traversal. An algorithm can operate on iterator ranges without knowing the underlying container. The conventional half-open range [first, last) includes first and excludes last. Iterator categories represent increasingly strong capabilities: - input: readable, single-pass traversal;
- output: writable, single-pass traversal;
- forward: multipass traversal;
- bidirectional: forward and backward movement;
- random access: jumps and distance in constant time;
- contiguous: random access with contiguous storage.
Passing ranges rather than entire containers improves composability: template <std::input_iterator It>
auto count_positive(It first, It last) {
using Value = std::iter_value_t<It>;
std::size_t count = 0;
for (; first != last; ++first)
if (*first > Value{}) ++count;
return count;
}The algorithm assumes that dereferenced values can be compared with a value-initialized Value. In production code, a more precise custom concept may be preferable. AlgorithmsAlgorithms separate operations from storage. Examples include std::sort, std::find, std::transform, std::accumulate, std::remove, and std::partition. std::vector<int> values{5, 1, 4, 2, 3};
std::sort(values.begin(), values.end());The complexity of comparison sorting is generally O(n log n), where n is the number of elements, under the standard algorithmic assumptions. The exact implementation and constant factors are library-dependent. std::stable_sort preserves the relative order of equivalent elements but may require additional memory. The erase-remove idiom illustrates the separation between algorithm and container: values.erase(std::remove_if(values.begin(), values.end(),
[](int x) { return x % 2 == 0; }),
values.end());remove_if rearranges the range and returns a new logical end; erase removes the physically redundant tail from the container.
RangesThe ranges library represents sequences more expressively and supports composable views: auto result = values
| std::views::filter([](int x) { return x > 0; })
| std::views::transform([](int x) { return x * x; });A view is generally lazy: it describes computation rather than immediately materializing a new collection. This can reduce allocations, but the source range must remain valid while the view is used. Lazy pipelines can also complicate debugging and lifetime reasoning. Function objects and lambdasA function object is an object callable like a function. Lambdas are concise function-object expressions and may capture state. Standard facilities such as std::less<>, std::greater<>, std::function, and std::invoke support generic invocation. std::function offers type erasure: it can store different callable types behind one runtime interface. This flexibility may involve allocation or indirect invocation, so templates are preferable in performance-critical, statically typed paths when type uniformity is not required.
Utilities and resource managementImportant generic utilities include: std::pair and std::tuple for heterogeneous grouping;std::optional<T> for an optional result;std::variant<T...> for a type-safe discriminated union;std::any for runtime type-erased storage;- smart pointers for ownership management;
std::move and std::forward for value-category control;- type traits and concepts for compile-time reasoning.
std::unique_ptr expresses exclusive ownership, while std::shared_ptr expresses shared ownership. Shared ownership should not be selected merely because it is convenient; reference counting introduces synchronization and lifetime complexity. std::weak_ptr breaks ownership cycles but does not itself keep an object alive.
7. Complexity, Correctness, and Exception SafetyGeneric code must specify more than syntax. Its contract should identify valid types, semantic assumptions, complexity, iterator invalidation, ownership, and exception behavior. For a container operation, relevant questions include: - Is the operation amortized, average-case, worst-case, or guaranteed constant time?
- Can reallocation invalidate references or iterators?
- Does the operation require copyability, movability, or default construction?
- What happens if an element operation throws?
- Is ordering strict and consistent?
The standard notion of a strict weak ordering is particularly important for sorting and ordered associative containers. A comparator must be consistent; a comparator that changes behavior unpredictably can violate algorithm assumptions and produce undefined or erroneous logical results. Exception-safety guarantees are commonly described as: - no-throw guarantee: the operation cannot fail by throwing;
- strong guarantee: failure leaves observable state unchanged;
- basic guarantee: invariants remain valid, though state may change;
- no guarantee: invariants may not be preserved, usually an undesirable design.
Generic components should use RAII and standard types to make ownership and cleanup automatic. noexcept should be specified when it is semantically true, because library components may use it to select more efficient operations. 8. Worked Application: A Generic Top-K ServiceConsider a service that receives records with a score and must retain the highest k records. A full sort costs approximately O(n log n) comparisons for n records. A min-heap of size k can process each record in O(log k), yielding approximately O(n log k) time and O(k) additional storage, assuming k is much smaller than n. #include <algorithm>
#include <functional>
#include <queue>
#include <vector>
struct Record {
int id;
double score;
};
std::vector<Record> top_k(std::vector<Record> input, std::size_t k) {
if (k == 0) return {};
auto worse = [](const Record& a, const Record& b) {
return a.score > b.score;
};
std::priority_queue<Record, std::vector<Record>, decltype(worse)>
heap(worse);
for (const auto& record : input) {
heap.push(record);
if (heap.size() > k) heap.pop();
}
std::vector<Record> result;
result.reserve(heap.size());
while (!heap.empty()) {
result.push_back(heap.top());
heap.pop();
}
std::sort(result.begin(), result.end(),
[](const Record& a, const Record& b) {
return a.score > b.score;
});
return result;
}AnalysisThe function is generic in algorithmic structure but uses a concrete Record type. It could be generalized by accepting iterators and a projection or comparator. The heap retains only the best k records because the least valuable retained record is removed whenever the size exceeds k. The final sort orders the selected records for presentation; without it, the output order would be heap-dependent. Important boundary conditions are handled explicitly: if k is zero, no heap is constructed for meaningful output. If k exceeds the number of records, all records are returned. A production implementation must also decide how ties are handled, whether copying records is acceptable, and whether scores containing NaN are permitted. 9. Illustrative Case Study: Generic Event ProcessingBackground. A university research platform processes events from laboratory devices, simulations, and student applications. An initial design contains separate queues and filtering functions for each event type. Problem. The duplicated code increases maintenance effort. Some queues use linked lists, others use vectors, and error handling differs. Performance is unpredictable, and adding a new event type requires editing central dispatch logic. Analysis. The common requirements are enqueue, dequeue, inspection of the next event, filtering, and transformation. Event payloads differ, but the queue policy is largely identical. Runtime polymorphism would require a common base event and virtual dispatch. A generic design can preserve the concrete payload type when compile-time knowledge is available. Approach. The team introduces a constrained queue adapter over a standard container, uses std::deque for frequent end operations, and exposes iterator ranges for generic algorithms. Concepts document requirements for event identifiers and timestamps. std::variant is used only at the external boundary where heterogeneous events must share one message channel. Internally, typed pipelines use templates and ranges. Outcome. Duplicate algorithms are removed, invalid event operations are detected earlier, and the most frequently used pipeline avoids virtual dispatch. The team also records container choices, complexity guarantees, and ownership rules in the interface documentation. Learning points. Generic programming is most successful when common semantics are identified first. It should not be applied indiscriminately: a type-erased or runtime-polymorphic boundary remains appropriate when plugins, dynamic loading, or independent binary deployment are required. 10. Challenges and LimitationsCompilation and code sizeEach distinct template specialization may generate code. Excessive instantiation can increase compilation time and binary size. Techniques such as minimizing header dependencies, using explicit instantiation where appropriate, applying type erasure at stable boundaries, and separating interface from implementation can help. Diagnostics and maintainabilityUnconstrained templates may produce diagnostics that expose deep implementation details. Concepts, smaller functions, named requirements, and focused tests improve maintainability. ABI and modulesTemplates are commonly defined in headers because the compiler needs their definitions at the point of instantiation. This can complicate binary interfaces. C++ modules improve dependency management and reduce repeated parsing, but module adoption requires compatible toolchains and disciplined build architecture. Semantic errorsA type may satisfy syntactic requirements while violating semantic ones. For example, a comparator may compile but fail to provide a stable ordering. Concepts cannot automatically prove all behavioral properties. Documentation, testing, assertions, and code review remain necessary. Security and governanceGeneric components can amplify a defect across many instantiations. Unsanitized input, unsafe ownership, integer overflow, iterator invalidation, and unchecked resource consumption remain security concerns. Organizations should apply static analysis, sanitizers, dependency governance, reproducible builds, and review of third-party library versions. Generic abstraction does not remove the need for secure coding. 11. Best Practices and Contemporary Direction- Prefer standard library facilities over reinventing common containers and algorithms.
- Express requirements with concepts and meaningful names.
- Choose containers based on access patterns, locality, invalidation, and ownership—not fashion.
- Prefer ranges and algorithms when they improve clarity, but make lifetimes explicit.
- Use
std::unique_ptr by default for exclusive dynamic ownership; use shared ownership only when shared lifetime is real. - Document complexity, exception safety, iterator invalidation, and semantic preconditions.
- Keep templates focused and avoid unnecessary metaprogramming cleverness.
- Test generic components with built-in types, move-only types, empty ranges, exceptional operations, and domain-specific types.
- Use compile-time computation when it improves correctness or performance, but measure compile-time and binary-size costs.
- Treat concepts, modules, ranges, constexpr evaluation, and sender/receiver-style asynchronous abstractions as part of the evolving modern C++ ecosystem, while checking compiler and library support before adoption.
AI-assisted programming can generate template code quickly, but generated code requires expert review. Particular risks include incorrect constraints, accidental copies, lifetime errors, invalid comparator semantics, and unjustified claims about complexity. AI tools should support—not replace—tests, profiling, security analysis, and design accountability. Chapter SummaryGeneric programming expresses algorithms and data structures in terms of operations and semantic requirements rather than concrete types. C++ templates provide compile-time parameterization through function templates, class templates, aliases, variable templates, and non-type parameters. Specialization enables exceptional behavior but should be used carefully. Concepts make generic interfaces clearer and diagnostics more useful. The Standard Library applies generic design through containers, iterators, algorithms, ranges, callable objects, utilities, and smart pointers. Effective use requires attention to complexity, locality, ownership, iterator invalidation, exception safety, and semantic correctness. Generic programming can improve reuse and performance, but it may increase compilation cost, code size, diagnostic complexity, and maintenance demands. Sound engineering balances compile-time abstraction with runtime polymorphism and type erasure where appropriate. Key TermsGeneric programming: Designing algorithms and data structures around requirements rather than concrete types. Template: A compile-time pattern from which functions, classes, or variables can be instantiated. Function template: A template that defines a family of functions. Class template: A template that defines a family of types. Template argument deduction: Compiler inference of template arguments from function-call arguments. Specialization: A customized implementation of a template for specified arguments. Concept: A named compile-time predicate describing template requirements. Constraint: A condition that controls whether a template or overload is viable. Iterator: An abstraction for traversing elements in a range. Range: A sequence represented commonly by a beginning and an end, or by a range object. Container: A library type that stores and manages a collection of objects. Algorithm: A reusable operation over iterators or ranges. View: A usually lazy, non-owning representation of a transformed or filtered range. Type trait: A compile-time facility that reports or transforms type properties. Type erasure: Hiding a concrete type behind a uniform runtime interface. RAII: Resource Acquisition Is Initialization; resource lifetime tied to object lifetime. Iterator invalidation: Loss of validity of an iterator, pointer, or reference after container modification. Strict weak ordering: Ordering relation required by many sorting and associative-container operations. Non-type template parameter: A compile-time value used as a template argument. Perfect forwarding: Preserving an argument’s value category when passing it onward. Review QuestionsShort Answer Questions- What problem does generic programming solve in object-oriented software?
- Distinguish a function template from a class template.
- Why are concepts preferable to relying only on substitution failure for template requirements?
- What is the purpose of the half-open iterator range
[first, last)? - What ownership relationship does
std::unique_ptr express?
Long Answer Questions- Compare generic programming and runtime polymorphism with respect to binding time, flexibility, performance, and binary stability.
- Explain template argument deduction, including the effects of references and explicit template arguments.
- Discuss the major Standard Library container categories and justify container selection using access patterns and locality.
- Explain iterator categories, algorithm composition, and the role of ranges and views in modern C++.
- Analyze the benefits and limitations of concepts, specialization, and type erasure in a large software system.
Analytical / Application Questions- A data-processing service receives one million records but must retain only the best 100. Compare full sorting with a bounded heap in terms of approximate time and additional storage. State your assumptions.
- Design a generic queue interface for move-only event objects. Identify its concepts, ownership policy, iterator invalidation rules, exception-safety guarantee, and suitable underlying containers.
- A team proposes replacing every virtual interface with templates to maximize performance. Critically evaluate the proposal for a plugin system whose implementations are loaded independently at runtime.
Multiple Choice Questions with AnswersQuestion 1Which mechanism most directly expresses compile-time requirements on a template in modern C++? A. A virtual destructor B. A concept or requires-clause C. A namespace alias D. A runtime exception Correct Answer: B. A concept or requires-clause Question 2What does the half-open range [first, last) contain? A. Only last B. Both first and last C. first through the element before last D. Neither first nor last Correct Answer: C. first through the element before last Question 3Which container normally provides contiguous storage and amortized constant-time insertion at the end? A. std::vector B. std::list C. std::map D. std::forward_list Correct Answer: A. std::vector Question 4Why is std::unique_ptr generally preferred over std::shared_ptr when ownership is exclusive? A. It supports multiple owners automatically B. It expresses exclusive ownership with less ownership-management overhead C. It never requires a complete type D. It performs runtime type erasure Correct Answer: B. It expresses exclusive ownership with less ownership-management overhead Question 5Which statement about a lazy range view is correct? A. It always creates a deep copy immediately B. It generally describes computation without necessarily materializing a new collection C. It owns every source element D. It can only operate on arrays Correct Answer: B. It generally describes computation without necessarily materializing a new collection Question 6A bounded heap retaining k elements processes n inputs. Under typical heap assumptions, its approximate processing complexity is: A. O(n log k) B. O(k log n) C. O(n²k) D. O(1) Correct Answer: A. O(n log k) Question 7Which facility is primarily used to represent one value selected from several alternative types? A. std::optional B. std::variant C. std::array D. std::weak_ptr Correct Answer: B. std::variant Question 8Why can a linked list be slower than a vector even when an insertion operation has a favorable asymptotic classification? A. Linked lists always sort themselves B. Vectors cannot store objects C. Linked lists often have poor cache locality and allocation overhead D. Vectors require virtual functions Correct Answer: C. Linked lists often have poor cache locality and allocation overhead Question 9Which guarantee means that an operation either succeeds or leaves observable state unchanged? A. Basic guarantee B. Strong guarantee C. No guarantee D. Input guarantee Correct Answer: B. Strong guarantee Question 10What is the principal role of type erasure? A. To expose every implementation detail B. To hide a concrete type behind a uniform runtime interface C. To force all types to inherit from one class D. To eliminate all runtime costs Correct Answer: B. To hide a concrete type behind a uniform runtime interface
|