C++ Programming
Templates, Generic Programming and C++17 Facilities
PGCP-AC
1. Generic Programming
Generic programming expresses an algorithm or data structure in terms of the operations it requires rather than one concrete type. A maximum algorithm needs comparison; a container needs a value type it can store and manage. Templates let the compiler create type-safe functions and classes for suitable arguments.
A template is a compile-time pattern. It is not an untyped runtime function. When a specialization is needed, the compiler substitutes template arguments and checks the resulting declarations and expressions.
Templates provide compile-time polymorphism. Virtual functions provide runtime subtype polymorphism. The two solve different problems and can be used together.
2. Function Templates
template<class T>
const T& larger(const T& a, const T& b) {
return a < b ? b : a;
}
This declaration describes a family of functions. T is a template type parameter. Calls such as larger(3, 7) and larger(name1, name2) request different specializations.
The body imposes requirements that are checked for each specialization. T must support the less-than expression and the conditional expression must yield a suitable result. A type that lacks the required operation cannot use this template successfully.
typename and class are equivalent when declaring a type parameter:
template<typename T>
void process(const T& value);
3. Template Argument Deduction
The compiler usually deduces function-template arguments from call arguments:
int x = larger(4, 9); // T is int
Each occurrence of T contributes constraints. In larger(1, 2.0), the first argument suggests int and the second suggests double. Deduction does not generally choose a common type, so the call fails.
The caller can state an argument explicitly:
double x = larger<double>(1, 2.0);
Both function arguments can then convert to double. A better generic interface may use two template parameters and derive a common return type when mixed values are intentionally supported.
4. Parameters, References and Deduction
Passing T by value drops top-level const and reference qualification and usually copies or moves the argument. Passing const T& avoids copying and accepts both const and nonconst lvalues.
A forwarding reference has the form T&& where T is deduced in the relevant context:
template<class T>
void relay(T&& value) {
destination(std::forward<T>(value));
}
Reference-collapsing and std::forward preserve whether the original argument was an lvalue or rvalue. This technique is powerful for wrappers and factories, but ordinary algorithms should use simpler parameter forms when they express the contract.
5. Return-Type Deduction
auto can let the compiler deduce a function's return type from return statements:
template<class A, class B>
auto add(const A& a, const B& b) {
return a + b;
}
A trailing return type is useful when the result refers to parameters:
template<class A, class B>
auto add(const A& a, const B& b) -> decltype(a + b) {
return a + b;
}
decltype applies language rules to an expression's type without evaluating the expression. Parentheses can affect its result for named variables because decltype distinguishes a declared entity from a general lvalue expression.
6. Class Templates
template<class T>
class Box {
public:
explicit Box(T value) : value_(std::move(value)) {}
const T& get() const { return value_; }
T& get() { return value_; }
private:
T value_;
};
Box<int> and Boxstd::string are distinct types created from the same class template. Each specialization has its own static data members and member-function instantiations.
Before C++17, template arguments for a class were normally written explicitly. C++17 class template argument deduction can infer them from constructors:
Box box(std::string("data"));
Deduction guides can define additional deduction rules where constructor deduction is insufficient.
7. Non-Type Template Parameters
A template parameter can represent a permitted compile-time value:
template<class T, std::size_t N>
class FixedArray {
public:
constexpr std::size_t size() const noexcept { return N; }
T& operator[](std::size_t i) { return values_[i]; }
private:
T values_[N]{};
};
FixedArray<int, 10> and FixedArray<int, 20> are different types. N can control storage, behavior and compile-time computation. C++17 permits a defined set of non-type parameter categories, including integral and enumeration values and certain pointers.
8. Default Template Arguments and Aliases
Templates can supply defaults:
template<class T, class Allocator = std::allocator<T>>
class Sequence;
An alias template gives a readable name to a family of types:
template<class T>
using StringMap = std::map<std::string, T>;
StringMap<int> counts;
Aliases do not create new distinct types; they name existing specializations. They help expose only the parameters a user should choose.
9. Instantiation and Visibility
Implicit instantiation occurs when code needs a template specialization. At that point the compiler must usually see the complete template definition, not merely a declaration. This is why template definitions are commonly placed in headers.
Separating a template definition into a source file can lead to unresolved symbols because another translation unit cannot instantiate code it cannot see. Explicit instantiation offers a controlled alternative:
template class Box<int>;
The corresponding extern template declaration can suppress repeated implicit instantiation elsewhere. This technique is useful when a library supports a fixed known set of types.
10. Full Specialization
A full specialization supplies an implementation for one exact argument combination:
template<class T>
struct Formatter {
static std::string format(const T& value);
};
template<>
struct Formatter<bool> {
static std::string format(bool value) {
return value ? "yes" : "no";
}
};
The specialized definition is no longer a template over T because T has been fixed as bool. Specializations should preserve the conceptual contract of the primary template.
11. Partial Specialization
Class templates can be partially specialized for a category of arguments:
template<class T>
struct Traits;
template<class T>
struct Traits<T*> {
static constexpr bool isPointer = true;
};
The specialization matches every pointer type. Variable templates also support partial specialization. Function templates do not support partial specialization in the same way. Use function overloading, tag dispatch or a specialized helper class instead.
12. Function-Template Overloading
Function templates participate in overload resolution with ordinary functions and other templates:
template<class T>
void print(const T& value);
template<class T>
void print(T* pointer);
void print(int value);
The compiler first determines viable candidates, compares conversions and then applies template ordering rules. A nontemplate is preferred only when it is otherwise an equally good match; it does not automatically defeat a better template match.
Overloading is generally clearer than explicit function-template specialization because it participates normally in overload resolution.
13. Variadic Templates
A template parameter pack represents zero or more arguments:
template<class... Ts>
void log(const Ts&... values);
Ts is a type pack and values is a function-parameter pack. An expansion applies a pattern to every pack element. Before fold expressions, recursive helper calls were commonly used to process packs.
C++17 fold expressions combine a pack with an operator:
template<class... Ts>
auto sum(Ts... values) {
return (values + ...);
}
A unary fold requires the operator and pack to make sense for the empty-pack case or the interface must prevent an empty call. A binary fold can supply an initial value:
return (0 + ... + values);
14. Type Traits
The type_traits header provides compile-time information and transformations:
static_assert(std::is_integral_v<int>);
template<class T>
using Bare = std::remove_cv_t<std::remove_reference_t<T>>;
Traits expose a value or a nested type. C++17 variable-template forms ending in _v and alias forms ending in _t shorten common uses.
Traits support generic decisions but should not create unnecessary branches. Prefer writing one operation that naturally works for all supported types when possible.
15. SFINAE
Substitution failure is not an error, abbreviated SFINAE, means that certain failures while substituting a candidate template remove that candidate from overload resolution instead of rejecting the whole program.
template<class T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
T twice(T value) {
return value * 2;
}
This overload exists only for integral T. SFINAE syntax can be difficult to read. C++20 concepts improve constraint expression, but in C++17 enable_if, void_t, traits and detection idioms are common.
16. if constexpr
An if constexpr condition is evaluated during compilation:
template<class T>
std::string describe(const T& value) {
if constexpr (std::is_integral_v<T>) {
return std::to_string(value);
} else {
return value.describe();
}
}
For a given specialization, the unselected branch is discarded. It does not need to be valid for that T when its invalidity depends on template parameters. A normal if would require both branches to be compiled.
Use if constexpr for a small, genuine compile-time variation. Separate overloads may communicate substantially different algorithms more clearly.
17. Structured Bindings
C++17 structured bindings introduce names for elements of an array, tuple-like value or accessible aggregate:
for (const auto& [key, value] : counts) {
std::cout << key << ": " << value << '\n';
}
The declaration can use auto, auto& or const auto& depending on whether it should copy, modify or observe the underlying elements. Omitting & may copy values, which matters for cost and mutation.
auto [iterator, inserted] = set.insert(value);
This makes multi-result operations easier to read without manual get calls.
18. Inline Variables
Before C++17, defining a nonconst namespace-scope variable in a header risked multiple definitions. An inline variable may be defined identically in multiple translation units while representing one program entity:
struct Limits {
inline static constexpr int maximum = 100;
};
Inline variables support header-defined constants and static template-related state. Inline does not mean the compiler must substitute the variable's value at each use; it primarily changes the definition rules here.
19. Other Useful C++17 Facilities
C++17 adds std::optional for a value that may be absent, std::variant for one value selected from a fixed set of alternatives and std::any for a type-erased value. std::string_view provides a nonowning view of character data and std::filesystem supplies portable path and directory operations.
These types encode intent more clearly than sentinels and unstructured void pointers. Their lifetime rules still matter. A string_view does not own its characters and references or views must not outlive the source.
Nested namespace syntax, initializer statements in if and switch and guaranteed copy elision in specified cases further improve expression and object construction.
20. Template Diagnostics and Design
Template errors can be long because a failure is reported through layers of instantiation. Start with the first meaningful message that identifies an invalid expression or missing operation for the supplied arguments. Later messages often show the path through library templates rather than a separate root cause.
Name template parameters according to their role, keep required operations small and use static_assert with a focused message when it clarifies a constraint. Avoid assuming that every T behaves like a built-in number.
Generic code succeeds when its requirements are precise. A template should work for every type that meets those requirements, reject types that do not, preserve value categories and ownership where needed and expose an interface that remains understandable without reading compiler machinery.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.