7. Templates and Generic ProgrammingIntroduction and ContextObject-oriented programming organizes software around abstractions such as classes, objects, inheritance, and polymorphism. These mechanisms allow a program to model different entities through common interfaces. However, object orientation alone does not eliminate repetition. A sorting algorithm, container, mathematical operation, or resource-management routine may have the same logic for many data types. Writing a separate version for `int`, `double`, `std::string`, and user-defined types is tedious and increases the possibility of inconsistent defects. C++ addresses this problem through templates and generic programming. A template describes a family of functions or classes parameterized by types, values, or compile-time properties. The compiler generates a concrete specialization when the template is used. Generic programming therefore moves reuse from inheritance hierarchies and runtime dispatch toward compile-time composition and type-safe abstraction. Modern C++ extends the original template model with non-type template parameters, partial and full specialization, variadic templates, fold expressions, type traits, `constexpr`, and concepts. These facilities make it possible to express sophisticated interfaces while rejecting invalid uses during compilation. This chapter develops these ideas progressively and examines their design implications in professional software. Learning ObjectivesAfter studying this chapter, the learner should be able to: - explain the purpose and mechanics of function and class templates;
- distinguish template instantiation, specialization, and overload resolution;
- design generic algorithms and data structures with suitable type requirements;
- use non-type parameters, partial specialization, and variadic templates;
- apply type traits, `constexpr`, concepts, and `requires` clauses;
- evaluate compile-time and runtime trade-offs in template-based designs;
- identify common errors involving deduction, forwarding, linkage, and diagnostics;
- select appropriate generic techniques for production-quality C++ systems.
Genericity as a Design IdeaA generic component separates an algorithm's structure from the specific types on which it operates. Consider a maximum operation. Its logic is independent of whether the operands are integers, floating-point values, or objects with an ordering relation: ```cpp template <typename T> T maximum(T a, T b) { return (a < b) ? b : a; } ``` Here `T` is a template type parameter. A call such as `maximum(3, 7)` allows the compiler to deduce `T` as `int`; `maximum(2.5, 1.2)` produces a version for `double`. The template is not itself an executable function for one particular type. It is a pattern from which the compiler forms a specialization. The terms are important. A template declaration introduces the pattern. Template argument deduction infers arguments from a call when possible. Instantiation creates a specialization for particular arguments. An implicit specialization is generated automatically, whereas an explicit specialization is written by the programmer for a specific case. The compiler checks the expressions used by a template when it instantiates that template, although modern constraints can make invalid uses fail earlier and more clearly. Generic programming is not simply the removal of types. A useful generic interface must identify the operations it needs, preserve meaningful semantics, and avoid accidental assumptions. For example, a sorting algorithm requires more than the existence of a type: it needs a way to compare elements, exchange or move them, and access a range. Function Templates and DeductionA function template can have one or more type parameters and ordinary function parameters: ```cpp template <class T> T add(T x, T y) { return x + y; } ``` `class` and `typename` are equivalent in a template parameter declaration. The call `add(2, 5)` deduces `T` as `int`. Deduction generally uses the form and type of the function arguments, but it does not perform every conversion that an ordinary function call might perform. Thus `add(2, 3.5)` cannot deduce one single `T` consistently. The caller may specify the argument explicitly, as in `add<double>(2, 3.5)`, or the interface may use two parameters: ```cpp template <typename T, typename U> auto add(T x, U y) { return x + y; } ``` The distinction between values, references, and forwarding references is central. A parameter `T` receives by value and may copy or decay an array. A parameter `const T&` avoids copying and accepts lvalues and rvalues. A parameter `T&&` in a deduced context is a forwarding reference; it can preserve whether the caller supplied an lvalue or rvalue. Together with `std::forward`, it supports perfect forwarding: ```cpp template <typename T, typename... Args> std::unique_ptr<T> make_object(Args&&... args) { return std::make_unique<T>(std::forward<Args>(args)...); } ``` Overload resolution considers both ordinary functions and function templates. A non-template is not automatically preferred in every situation; the best viable candidate is selected according to the language's ranking rules. Ambiguous overload sets are a design problem, not merely a compiler inconvenience, and should be reduced through clear parameter types or constraints. Class Templates and Policy-Based StructureClass templates parameterize data representation and behavior: ```cpp template <typename T, std::size_t Capacity> class FixedBuffer { T data_[Capacity]{}; std::size_t size_ = 0; public: void push(const T& value) { if (size_ == Capacity) throw std::overflow_error("buffer full"); data_[size_++] = value; } std::size_t size() const { return size_; } const T& operator[](std::size_t i) const { return data_[i]; } }; ``` `FixedBuffer<int, 16>` and `FixedBuffer<double, 16>` are distinct types. `Capacity` is a non-type template parameter: it is a compile-time value rather than a type. Such parameters are useful for array dimensions, buffer sizes, bit masks, and algorithmic policies. A class template may define nested types, static members, constructors, and member templates. Template parameters may also represent policies. For example, a storage class might accept an allocation policy, a synchronization policy, or an error-handling policy. This approach is called policy-based design. It provides compile-time customization without a large inheritance hierarchy, but excessive policies can make interfaces difficult to understand and increase compilation costs. Template definitions normally belong in header files because the compiler needs the definition when it instantiates a specialization. Explicit instantiation can place selected generated versions in one implementation file, reducing repeated compilation. The `extern template` declaration can suppress unwanted implicit instantiation in other translation units. Specialization: Exceptions to the General PatternA general template is appropriate when one algorithm works uniformly. Sometimes a particular type requires a different representation or algorithm. Full specialization supplies a definition for an exact set of template arguments: ```cpp template <typename T> struct Serializer { static std::string write(const T& value) { return std::to_string(value); } }; template <> struct Serializer<bool> { static std::string write(bool value) { return value ? "true" : "false"; } }; ``` Partial specialization applies to a family of types and is available for class templates, not function templates. It is useful for recognizing pointers, arrays, or particular template forms: ```cpp template <typename T> struct IsPointer { static constexpr bool value = false; }; template <typename T> struct IsPointer<T*> { static constexpr bool value = true; }; ``` In modern C++, a concept or `if constexpr` is often clearer than specialization when the difference is only an implementation branch. Specialization should represent a meaningful semantic or performance distinction, not merely work around an avoidable overload problem. Variadic Templates and Parameter PacksA variadic template accepts an arbitrary number of template arguments. A parameter pack represents zero or more arguments and must be expanded in a suitable context. A recursive implementation illustrates the basic idea: ```cpp void print_all() {} template <typename First, typename... Rest> void print_all(const First& first, const Rest&... rest) { std::cout << first << ' '; print_all(rest...); } ``` C++17 fold expressions provide a concise alternative: ```cpp template <typename... Args> auto sum(Args... args) { return (args + ...); } ``` The expression `(args + ...)` is a unary right fold. Other forms include `(... + args)`, `(init + ... + args)`, and `(args + ... + init)`. The choice matters for empty packs and associativity. A fold over `&&` or `||` also preserves short-circuit behavior in the appropriate expression context. Variadic templates underpin tuples, forwarding factories, logging interfaces, visitor utilities, and compile-time registration systems. Their power should be balanced against readability. A named abstraction with a constrained interface is often preferable to a deeply nested pack transformation. Compile-Time Computation and Type TraitsTemplates can compute values and types during compilation. A non-type parameter can encode a dimension, while `constexpr` functions allow evaluation when inputs are constant expressions: ```cpp constexpr unsigned factorial(unsigned n) { return n <= 1 ? 1 : n * factorial(n - 1); } static_assert(factorial(5) == 120); ``` `static_assert` turns a compile-time condition into a documented invariant. The standard library's type traits, located primarily in `<type_traits>`, expose properties such as `std::is_integral_v<T>`, `std::is_same_v<T, U>`, and `std::is_nothrow_move_constructible_v<T>`. Transformation traits include `std::remove_reference_t<T>`, `std::decay_t<T>`, and `std::underlying_type_t<Enum>`. Before concepts, programmers commonly used SFINAE—Substitution Failure Is Not An Error—to remove invalid overloads from consideration. Techniques such as `std::enable_if_t` remain relevant in legacy code, but they often produce complex declarations and poor diagnostics. Concepts express the same intent more directly. Concepts and Constraints in Modern C++A concept is a named compile-time predicate describing requirements on template arguments: ```cpp template <typename T> concept Addable = requires(T a, T b) { { a + b } -> std::convertible_to<T>; }; template <Addable T> T combine(T a, T b) { return a + b; } ``` The standard concepts are defined in `<concepts>`. A `requires` expression tests whether expressions are valid and can impose result-type or nested requirements. Constraints may be placed after the template parameter list: ```cpp template <typename R> requires std::ranges::range<R> void inspect(R&& range); ``` Constraints improve interfaces in three ways. First, they document semantic expectations near the declaration. Second, they participate in overload ordering, allowing a more constrained overload to be preferred. Third, they produce diagnostics that identify the violated requirement rather than displaying a long instantiation trace. A concept cannot prove an informal semantic law. `std::equality_comparable<T>` can check the availability of equality expressions, but it cannot guarantee that equality is transitive. Consequently, generic code requires both syntactic constraints and documented semantic contracts. Concepts are a complement to testing and design reasoning, not a replacement for them. A Method for Designing Generic InterfacesA disciplined design process is more reliable than beginning with elaborate template syntax. 1. Identify the invariant algorithm or abstraction. Separate what is genuinely common from what varies. 2. Decide whether variation is a type, compile-time value, policy, or runtime object. Compile-time variation is not automatically better. 3. State the required operations and semantic assumptions. Prefer a small concept to an unnecessarily broad interface. 4. Choose parameter passing deliberately. Preserve references only when identity or performance requires it; use forwarding when constructing or forwarding arbitrary arguments. 5. Test boundary cases: empty ranges, incompatible types, zero sizes, const objects, lvalues, rvalues, and exception paths. 6. Examine generated code and compile-time impact. Genericity can improve optimization, but excessive instantiation can increase binary size. 7. Provide examples and diagnostics. A generic component is a public contract, not merely a clever implementation. The C++20 ranges library demonstrates this method. Algorithms operate on ranges or iterator-sentinel pairs and constrain the operations required. A sorting algorithm does not demand that every type inherit from a common base; it requires a suitable ordering and movable elements. This is structural, or compile-time, polymorphism. Applied Scenario: A Type-Safe Telemetry PipelineThe following illustrative case concerns a fictional industrial monitoring company. Its original telemetry library contained separate functions for integer, floating-point, and string measurements. Each function validated values, formatted them, and appended them to a message. Maintenance teams found that changes to validation were applied inconsistently. A proposed redesign used a generic pipeline. Background and problem. Sensors produce values of different types. The pipeline must accept a range of records, reject invalid values, and serialize accepted values. Some devices send batches of variable length, while embedded devices require fixed-capacity storage. The organization also wants compilation to fail when a sensor type has no valid serializer. Analysis. A runtime base class could represent all sensors, but it would introduce virtual dispatch and require a common erased representation. The core operations are known at compile time, so a constrained generic design is appropriate. The team separates the range algorithm, validation policy, and serialization policy. ```cpp template <typename T> concept Serializable = requires(const T& value) { { serialize(value) } -> std::convertible_to<std::string>; }; template <std::ranges::input_range R> requires Serializable<std::ranges::range_value_t<R>> std::vector<std::string> encode(R&& records) { std::vector<std::string> result; for (const auto& record : records) result.push_back(serialize(record)); return result; } ``` A separate `FixedBuffer<T, N>` is used by embedded targets. A full specialization handles a compact Boolean encoding, while ordinary overloads handle numeric and textual values. Tests verify empty input, unsupported types, extreme values, and serialization consistency. Outcome. The duplication between sensor-specific functions is removed, unsupported record types produce a focused constraint error, and the same algorithm serves both standard-library containers and the company's fixed buffer after the buffer is given suitable range access. The design does not claim that all concerns are compile-time concerns: device configuration and validation thresholds remain runtime data. Learning points. Generic design is successful because the team identified stable operations, expressed requirements explicitly, and kept policy variation separate. It would be unsuccessful if every minor option became a template parameter or if semantic validation were assumed merely because an expression compiled. Practical Uses and Professional ImplicationsTemplates are used throughout professional C++ software: standard containers and algorithms, numerical libraries, embedded buffers, serialization systems, smart pointers, expression-template libraries, parsers, testing frameworks, and high-performance financial or scientific systems. They support zero-cost abstractions when the compiler can specialize and optimize code without runtime indirection. For teams, template design affects build architecture. Header-only libraries are convenient but may increase preprocessing and instantiation work. Modules and explicit instantiation can improve boundaries, although portability and toolchain support must be considered. Public templates also form an API contract: changing a constraint, default argument, or overload can alter source compatibility and overload resolution. Generic code must be reviewed for safety. Unconstrained templates may accidentally accept dangerous conversions, signed/unsigned combinations, dangling references, or types with surprising operator meanings. A forwarding function can preserve an rvalue reference to an object whose lifetime ends too soon if ownership is not designed correctly. Compile-time checks reduce certain errors, but they do not prevent data races, invalid external input, or incorrect business rules. Common Mistakes and LimitationsA frequent mistake is assuming that syntactic validity implies semantic validity. Another is using `std::move` unconditionally in a template, which can turn an lvalue supplied by the caller into a moved-from object; `std::forward` is appropriate only when preserving forwarding-reference categories. Other recurring problems include: - placing a template definition only in a source file and then receiving an unresolved or unavailable instantiation;
- expecting deduction to combine unrelated argument types automatically;
- creating ambiguous overloads by adding unconstrained generic functions;
- specializing function templates when a constrained overload would be clearer;
- using recursive variadic templates where a fold expression is simpler;
- generating excessive code through many type and value combinations;
- exposing implementation-heavy diagnostics through an unconstrained public API;
- overlooking exception guarantees, ownership, and thread safety because attention is focused on type parameters.
Templates also have real limitations. Error messages can remain complex in highly dependent code, compilation can be slow, and binary size can grow. Dynamic systems or plugin boundaries often need type erasure, stable ABI interfaces, or runtime polymorphism instead. The appropriate question is not whether templates are more modern, but where compile-time knowledge creates a genuine advantage. Ethical, Governance, and Quality ConsiderationsGeneric infrastructure is often reused across products, so defects can have a wide blast radius. Organizations should require code review, automated tests, static analysis, reproducible builds, and documented compiler standards. Constraints should communicate safe usage rather than merely silence warnings. Where templates process untrusted input, validation and resource limits remain mandatory; compile-time type safety does not validate external data. Open-source template libraries also carry licensing and supply-chain responsibilities. Teams should track versions, review transitive dependencies, and avoid copying specialized metaprogramming code without understanding its maintenance and security implications. In regulated systems, generated behavior should be traceable through tests and build artifacts, even when much of the implementation is produced by instantiation. Current Developments and Best PracticesModern C++ development increasingly favors concepts, ranges, `constexpr` algorithms, and standard library facilities over custom SFINAE and macro-based genericity. `std::span`, `std::expected`, ranges, and constrained algorithms encourage interfaces based on views, values, and explicit failure handling. Reflection is an active area of language research, but portable production code should rely on standardized facilities available in the project's selected language version. Recommended practice is to constrain public templates, keep concepts close to the operations they describe, prefer simple generic algorithms over metaprogramming puzzles, and use `static_assert` for critical invariants. Measure compile time and generated code rather than assuming that a template is automatically efficient. Provide ordinary usage examples, test representative user-defined types, and preserve a runtime alternative when deployment, ABI, or configurability requires it. Chapter SummaryTemplates allow C++ programs to express algorithms and data structures independently of particular types or compile-time values. Function templates rely on deduction and overload resolution; class templates support reusable representations, non-type parameters, and policy-based design. Full and partial specialization handle genuine exceptional cases, while variadic templates and fold expressions process arbitrary argument lists. Type traits and `constexpr` support compile-time computation, and concepts with `requires` clauses state valid interfaces more clearly than traditional SFINAE. Effective generic programming combines syntactic constraints with semantic contracts, deliberate parameter passing, testing, and attention to build and binary costs. Templates are powerful in libraries, embedded systems, numerical software, and infrastructure, but runtime polymorphism and type erasure remain appropriate when variation is dynamic or ABI stability is essential. Key TermsTemplate: A compile-time pattern for generating functions or classes. Generic programming: Designing algorithms and abstractions independently of specific data types. Template parameter: A type, value, or template supplied to a template definition. Instantiation: Creation of a concrete template specialization for particular arguments. Template argument deduction: Inferring template arguments from function-call arguments. Non-type template parameter: A compile-time value used as a template argument. Full specialization: A definition customized for one exact combination of template arguments. Partial specialization: A class-template definition customized for a family of argument patterns. Parameter pack: A template parameter representing zero or more arguments. Fold expression: A C++17 expression that applies an operator across a parameter pack. Type trait: A compile-time class or variable template describing or transforming a type. `constexpr`: A declaration permitting evaluation during compilation when conditions allow. SFINAE: The rule by which invalid substitution removes a candidate instead of causing immediate failure. Concept: A named compile-time predicate specifying requirements on template arguments. Requires-expression: An expression that tests whether types and expressions satisfy requirements. Forwarding reference: A deduced `T&&` parameter that preserves an argument's value category. Perfect forwarding: Passing an argument onward while preserving its lvalue or rvalue nature. Policy-based design: Customizing a class or algorithm through compile-time policy parameters. Structural polymorphism: Polymorphism based on supported operations rather than inheritance. Type erasure: Representing values through a runtime interface while hiding their concrete types. Review QuestionsShort Answer Questions1. What is the difference between a template and an instantiated specialization? 2. Why might a function template fail to deduce one common type for two arguments? 3. Distinguish full specialization from partial specialization. 4. What problem do concepts solve compared with unconstrained templates? 5. What is the purpose of a fold expression? Long Answer Questions1. Explain function-template deduction, forwarding references, and perfect forwarding with suitable code examples. 2. Discuss the design and implementation considerations of a class template with a non-type parameter. 3. Compare SFINAE, type traits, `if constexpr`, and concepts as techniques for constraining generic code. 4. Explain how variadic templates and fold expressions are used to construct generic interfaces. 5. Analyze the benefits and costs of compile-time polymorphism compared with runtime polymorphism. Analytical / Application Questions1. Design a constrained generic function that accepts a range whose elements can be converted to a textual representation. State the concept and explain your parameter choices. 2. A library has separate functions for `int`, `double`, and `long` values but different behavior is required only for Boolean values. Decide whether to use overloads, specialization, or a concept-based branch, and justify the choice. 3. A template-heavy application has slow builds and a large executable. Propose a technically justified strategy involving explicit instantiation, constraints, interface simplification, or runtime alternatives. 22. Multiple Choice Questions with AnswersQuestion 1When calling `maximum(3, 7)`, what normally happens to the template parameter `T`? A. It is deduced as `double` B. It is deduced as `int` C. It must always be specified explicitly D. It is deduced as `void` Correct Answer: B. It is deduced as `int` Question 2Which statement about partial specialization is correct? A. It applies only to function templates B. It customizes a class template for a family of type patterns C. It must define behavior for every possible type D. It is identical to function overloading Correct Answer: B. It customizes a class template for a family of type patterns Question 3What is the principal purpose of a non-type template parameter such as `Capacity`? A. To store a runtime value in every object B. To provide a compile-time value to a template C. To replace all class data members D. To force dynamic allocation Correct Answer: B. To provide a compile-time value to a template Question 4Which feature provides a concise way to apply an operator over a parameter pack? A. A fold expression B. A virtual function C. A namespace alias D. A destructor Correct Answer: A. A fold expression Question 5Why are template definitions commonly placed in header files? A. The linker cannot process any source files B. The compiler generally needs the definition when instantiating a specialization C. Headers execute before source files at runtime D. Templates cannot contain member functions Correct Answer: B. The compiler generally needs the definition when instantiating a specialization Question 6What does a concept primarily express? A. A runtime inheritance relationship B. A compile-time predicate describing template requirements C. A memory allocation strategy D. A mandatory virtual table layout Correct Answer: B. A compile-time predicate describing template requirements Question 7Which technique is generally preferable to complex SFINAE when expressing modern C++ template requirements? A. Preprocessor substitution B. Concepts and requires clauses C. Global variables D. Manual casts Correct Answer: B. Concepts and requires clauses Question 8In a forwarding function, why is `std::forward<T>(value)` used? A. To convert every argument to `const` B. To preserve the caller's lvalue or rvalue category C. To allocate the argument dynamically D. To disable template deduction Correct Answer: B. To preserve the caller's lvalue or rvalue category Question 9Which situation most strongly suggests using runtime polymorphism rather than templates? A. The types are known at compile time and require different optimized code B. A fixed buffer size should be encoded in its type C. The set of types must be selected dynamically across a stable ABI boundary D. A compile-time invariant can be checked with `static_assert` Correct Answer: C. The set of types must be selected dynamically across a stable ABI boundary Question 10Why are concepts alone insufficient to guarantee correct generic behavior? A. They cannot check whether required expressions are syntactically valid B. They cannot prove all informal semantic laws, such as transitivity of equality C. They operate only at runtime D. They prevent all user-defined types Correct Answer: B. They cannot prove all informal semantic laws, such as transitivity of equality
|