Generic Programming
Generic programming is the discipline of writing algorithms and data structures around what a type can do rather than what it is — and templates are the language mechanism C++ built specifically to make that discipline pay off at zero runtime cost.
Object-oriented programming organizes code around what an object is — a Shape, an Animal, a Widget — and shares behavior through inheritance and virtual dispatch resolved at runtime. Generic programming organizes code around what a type can do — is it comparable, is it iterable, can it be added — and shares behavior through templates, resolved entirely at compile time. The C++ standard library is the single best illustration of generic programming done well: std::sort, std::find, and std::accumulate each work correctly on a std::vector<int>, a std::list<std::string>, or a raw array, not because those containers share a common base class, but because each algorithm is written generically against the operations a type or an iterator is required to support. This page ties together why that's a different paradigm from object-oriented design, how the standard library's containers, iterators, and algorithms divide the generic-programming problem into independent, recombinable pieces, and previews the tools — type traits, compile-time programming, and concepts — that make constraining and reasoning about generic code tractable.
What Generic Programming Means
Generic programming means writing an algorithm or a data structure once, in terms of the operations it needs from a type, rather than in terms of one specific type. A generic sorting algorithm doesn't need to know it's sorting integers versus strings versus custom Employee objects — it only needs the ability to compare two elements with operator<. Anything that supports that one operation can be sorted by the exact same code.
This is a different axis of code reuse from object-oriented polymorphism. OOP shares behavior through a common base class and virtual functions resolved at runtime; generic programming shares behavior through a shared set of required operations, checked and resolved entirely at compile time, with no base class relationship required between the types involved at all.
Generic Algorithms
The <algorithm> header's functions are the clearest example: std::sort, std::find, std::count, and std::accumulate are each written once, generically, and work unmodified across completely different container types, as long as those containers expose compatible iterators.
std::sort and std::accumulate don't know or care that nums happens to be a vector — they operate purely through the iterator interface begin()/end() exposes, which is exactly why the same std::accumulate call works identically on a vector, a list, or an array.
#include <algorithm>
#include <numeric>
#include <vector>
#include <list>
#include <string>
#include <iostream>
int main()
{
std::vector<int> nums = {5, 1, 4, 2, 3};
std::sort(nums.begin(), nums.end());
std::list<std::string> words = {"pear", "apple", "kiwi"};
words.sort(); // list has its own sort — no random-access iterators available
int total = std::accumulate(nums.begin(), nums.end(), 0);
std::cout << total << "\n"; // 15
return 0;
}Generic Data Structures
The container side of the standard library is generic in exactly the same sense: std::vector<T>, std::stack<T>, and std::map<K, V> are each a single implementation, written once as a class template, that produces an independently type-checked container for whatever T (or K and V) you instantiate it with — precisely the class-template mechanism from the Templates page.
The STL Design Philosophy
The Standard Template Library is built on a deliberate separation of concerns into three independent pieces: containers store elements, algorithms operate on ranges of elements, and iterators are the glue between them — a uniform way to walk through any container's elements without an algorithm needing to know that container's internal layout at all.
my_find works on any Iterator type that supports !=, ++, and * — a raw pointer, a std::vector<int>::iterator, or a std::list<std::string>::iterator all qualify, so this one function template works across every container that exposes such an iterator, with zero code duplicated per container.
Because algorithms depend only on the iterator interface and never on a container's internal storage, adding a new container type to the library doesn't require rewriting a single algorithm — it only needs to provide iterators with the right capabilities, and every existing generic algorithm works with it immediately. That decoupling is the STL's central design achievement.
#include <iostream>
// A generic algorithm written directly against iterators, not against
// any specific container type
template <typename Iterator, typename T>
Iterator my_find(Iterator first, Iterator last, const T &value)
{
for (Iterator it = first; it != last; ++it)
{
if (*it == value)
return it;
}
return last;
}Constraints on Generic Code
Generic code always has implicit requirements, even when nothing in the code states them directly — my_max from the Templates page silently requires that T support operator>, and my_find above silently requires that the iterator type support !=, ++, and *. Calling either one with a type that doesn't satisfy those requirements still fails to compile, but historically the error message pointed deep inside the template's implementation rather than at the actual missing capability.
Nothing in my_max's signature announces that T needs operator> — that requirement only becomes visible when the compiler tries (and fails) to compile the template body for this particular T, producing an error message that's often long and hard to trace back to the real cause.
Concepts, a C++20 feature covered in full on its own page, exist specifically to make requirements like these explicit, checkable, and readable directly in a template's signature, so both the compiler's error messages and a human reader's understanding of what a generic function actually needs improve dramatically.
struct NoComparison
{
int value;
};
// my_max(NoComparison{1}, NoComparison{2}); would fail to compile,
// because NoComparison has no operator> — but pre-C++20, the error
// points into the body of my_max itself, not at the actual missing requirementType Traits and Compile-Time Programming
The <type_traits> header provides a large collection of compile-time predicates about types — std::is_integral<T>, std::is_pointer<T>, std::is_same<T, U> — each of which evaluates to true or false entirely during compilation, with zero runtime cost. Generic code can use these to make compile-time decisions about which implementation to generate for a given T.
if constexpr evaluates its condition at compile time and discards the branch that doesn't apply entirely — for describe<int>(), the floating-point and "something else" branches aren't even compiled into that instantiation, which is what makes this compile-time programming rather than an ordinary runtime if.
#include <type_traits>
#include <iostream>
template <typename T>
void describe()
{
if constexpr (std::is_integral_v<T>)
std::cout << "an integral type\n";
else if constexpr (std::is_floating_point_v<T>)
std::cout << "a floating-point type\n";
else
std::cout << "something else\n";
}
int main()
{
describe<int>(); // an integral type
describe<double>(); // a floating-point type
describe<std::string>(); // something else
return 0;
}Concepts (Preview)
C++20's concepts formalize exactly the kind of implicit requirement seen above into a named, checkable constraint that appears directly in a template's signature — for example, requiring T to satisfy the standard std::integral concept turns a silent assumption into an explicit, compiler-verified, and human-readable part of the function's interface. The Concepts page on this site covers the full syntax and standard library concepts in depth.
How does generic programming's approach to code reuse differ from object-oriented programming's approach?
Object-oriented programming shares behavior through a common base class and virtual dispatch resolved at runtime — types are related by inheritance. Generic programming shares behavior through a shared set of required operations (like supporting operator< or exposing a compatible iterator), checked and resolved entirely at compile time, with no inheritance relationship required between the types involved.
Why can std::sort operate correctly on a std::vector<int>, a std::deque<double>, and a plain C array, without three separate implementations?
std::sort is written generically against iterators rather than against any specific container type — it only relies on operations like !=, ++, *, and random access that a compatible iterator provides. Any container (or raw array, via pointers acting as iterators) that exposes iterators supporting those operations can be sorted by the exact same algorithm code.
What role do iterators play in the STL's three-part design of containers, algorithms, and iterators?
Iterators are the decoupling layer between containers and algorithms — a uniform interface for walking through a sequence of elements that hides each container's actual internal storage. Algorithms are written only against that iterator interface, so a new container type can be added to the library without rewriting any existing algorithm, as long as it provides iterators with the needed capabilities.
What is a type trait, and what does std::is_integral_v<T> let generic code do that it couldn't do otherwise?
A type trait is a compile-time predicate about a type, evaluated entirely during compilation with zero runtime cost. std::is_integral_v<T> lets generic code branch on whether T is an integral type at compile time (often with if constexpr), so a template can generate a different implementation path depending on the type it's instantiated with, without any runtime type-checking overhead.
Why did unconstrained templates before C++20 tend to produce confusing compiler errors when called with an unsuitable type?
A template's implicit requirements (like needing operator> or a comparable iterator) aren't stated anywhere in its signature — the compiler only discovers a type doesn't satisfy them when it tries to compile the template body for that specific type, so the resulting error message points deep inside the template's implementation rather than clearly naming the missing capability. Concepts were introduced specifically to make these requirements explicit and produce much clearer diagnostics.
Write a generic function template sumAll(const Container &c) that returns the sum of all elements in any container supporting begin()/end() and works correctly for both a std::vector<int> and a std::vector<double>.
sumAll is written entirely against the range-based for loop and Container::value_type, both of which any standard container provides, so the exact same template body is instantiated once for std::vector<int> and once for std::vector<double>, each producing a correctly typed sum without any container-specific code.
What does this program print, and why does classify(&x) take a different branch than classify(x)?
It prints "not a pointer" then "pointer". classify(x) instantiates classify<int>, where std::is_pointer_v<int> is false at compile time. classify(&x) instantiates classify<int*>, where std::is_pointer_v<int*> is true — if constexpr picks the matching branch entirely at compile time based on the deduced template argument, discarding the other branch from that particular instantiation.
Write a generic function template largestIn(const std::vector<T> &v) that returns the largest element of a vector of any comparable type, and demonstrate it with a vector of ints and a vector of strings.
largestIn relies only on operator> being defined for T — true for int via built-in comparison and true for std::string via lexicographic comparison — so the identical template code correctly finds the largest int and the lexicographically largest string, without any type-specific logic written anywhere.
#include <type_traits>
#include <iostream>
template <typename T>
void classify(T value)
{
if constexpr (std::is_pointer_v<T>)
std::cout << "pointer\n";
else
std::cout << "not a pointer\n";
}
int main()
{
int x = 5;
classify(x);
classify(&x);
return 0;
}