Generic programming is the discipline of writing code that is parameterised over types, so that the same algorithm or data structure can work with int, double, a custom class, or anything else that satisfies a required interface — without duplicating source code and without sacrificing runtime performance. This document goes from first principles through the most advanced techniques available in each language.
Before We Begin — The Problem Generics Solve
Imagine you want a function that returns the larger of two numbers. Without generics you have to write a separate function for every type you care about:
// Without generics — one copy per type, identical logic repeatedintmax_int (int a, int b) { return a > b ? a : b; }
doublemax_double(double a, double b) { return a > b ? a : b; }
floatmax_float (float a, float b) { return a > b ? a : b; }
// ... and again for long, char, short, every user-defined type...
Every new type means copy-pasting the same body. A bug fix must be applied to every copy. Generics solve this by letting you write the logic once and have the compiler or preprocessor generate the type-specific versions automatically:
// C — macro: works for any type that supports >#define MAX(a, b) ((a) > (b) ? (a) : (b))int x = MAX(3, 10); // works for intdouble y = MAX(1.5, 2.7); // works for double
// C++ — function template: type-safe, one definition, any comparable typetemplate <typename T>
T max_val(T a, T b) { return a > b ? a : b; }
int x = max_val(3, 10); // compiler generates an int versiondouble y = max_val(1.5, 2.7); // compiler generates a double version
std::string s = max_val(std::string("apple"), std::string("banana"));
// works for std::string too — no extra code written
That's the core idea: write once, use for any type. The rest of this page shows every tool both languages provide to achieve this, from the simplest macro to advanced C++20 concepts.
Generic programming is parametric abstraction: the algorithm is written once and the concrete type is supplied later — at the call site, at compile time, or at runtime depending on the mechanism. The goal is maximum code reuse with zero or near-zero overhead.
Three models of polymorphism
Model
Resolution time
Mechanism (C++)
Mechanism (C)
Cost
Parametric (generics)
Compile-time
Templates / concepts
Macros, X-macros
Zero (code duplication by compiler)
Subtype (OOP)
Runtime
Virtual functions
Function pointers in structs
vtable indirection, cache misses
Ad-hoc (overloading)
Compile-time
Function overloading
_Generic
Zero
What a type must satisfy: type requirements
Every generic algorithm implicitly or explicitly states what operations it needs from its type parameter. In C++ these are called concepts (formally since C++20). Examples:
Sortable: elements must be comparable with <.
Hashable: elements must support a hash function and equality.
Copyable: elements must be copy-constructible.
Violating a type requirement produces a compiler error (C++) or undefined behaviour / wrong result (C).
Zero-overhead abstraction
The C++ standard committee's guiding principle is: "What you don't use, you don't pay for, and what you do use, you couldn't have written better by hand." Templates enforce this by generating specialised machine code for every instantiated type — no boxing, no virtual dispatch, no runtime type checks unless the programmer explicitly requests them.
2. Generics in C
C has no built-in generic mechanism. Generic design in C relies on four complementary idioms: macros, void*-based APIs, X-Macros, and the C11 _Generic expression.
2a. Macros — from simple to advanced
Simple function-like macros
#define MAX(a, b) ((a) > (b) ? (a) : (b))#define MIN(a, b) ((a) < (b) ? (a) : (b))#define SWAP(T, a, b) do { T _tmp = (a); (a) = (b); (b) = _tmp; } while(0)int main() {
int x = MAX(3, 10); // expands: ((3) > (10) ? (3) : (10))double y = MIN(3.5, 2.1);
int a = 5, b = 9;
SWAP(int, a, b); // a == 9, b == 5
}
Pitfall 1 — double evaluation:MAX(i++, j++) increments both operands twice because the macro expands each argument wherever it appears. Use inline functions in C99+ or _Generic dispatch instead.
Pitfall 2 — operator precedence: always wrap every argument and the whole expression in parentheses. #define SQ(x) x*x gives SQ(1+2) = 1+2*1+2 = 5, not 9.
Pitfall 3 — no scope / hygiene: the _tmp variable in SWAP could shadow a caller variable with the same name. The do { ... } while(0) idiom at least enforces a single statement boundary.
A common C pattern generates a complete, type-specific data structure by wrapping struct and function definitions in a macro. Each macro invocation stamps out a new concrete type.
Each DEFINE_VEC call generates a fully separate, type-safe set of functions. The compiler sees real typed code, so pointer arithmetic and size calculations are all correct. This is essentially how small C generic libraries work.
2b. void* + size + callbacks — the qsort pattern
The C standard library uses this pattern throughout: pass a void* to the data, a size_t for element size, and a function pointer for type-specific operations.
Standard qsort / bsearch
#include<stdlib.h>#include<string.h>intcmp_int(const void* a, const void* b) {
int x = *(const int*)a, y = *(const int*)b;
return (x > y) - (x < y); // branchless three-way compare
}
intcmp_str(const void* a, const void* b) {
return strcmp(*(const char**)a, *(const char**)b);
}
int main() {
int nums[] = {5, 3, 9, 1};
qsort(nums, 4, sizeof(int), cmp_int);
const char* words[] = {"banana", "apple", "cherry"};
qsort(words, 3, sizeof(char*), cmp_str);
}
Building a generic stack with void*
/* generic_stack.h */typedef struct {
unsigned char* buf; // raw byte buffer
size_t top; // index of next free slot (in elements)
size_t cap; // capacity in elements
size_t esize; // size of one element in bytes
} GenStack;
voidgs_init(GenStack* s, size_t elem_size, size_t init_cap) {
s->esize = elem_size;
s->cap = init_cap;
s->top = 0;
s->buf = malloc(elem_size * init_cap);
}
voidgs_push(GenStack* s, const void* elem) {
if (s->top == s->cap) {
s->cap *= 2;
s->buf = realloc(s->buf, s->esize * s->cap);
}
memcpy(s->buf + s->top * s->esize, elem, s->esize);
s->top++;
}
intgs_pop(GenStack* s, void* out) {
if (s->top == 0) return0;
s->top--;
memcpy(out, s->buf + s->top * s->esize, s->esize);
return1;
}
voidgs_free(GenStack* s) { free(s->buf); }
/* ----- usage ----- */int main() {
GenStack s;
gs_init(&s, sizeof(double), 8);
double v = 3.14;
gs_push(&s, &v);
v = 2.72;
gs_push(&s, &v);
double out;
while (gs_pop(&s, &out)) printf("%.2f\n", out);
gs_free(&s);
}
Alignment risk:unsigned char* buffers are correctly aligned for char but may not be for larger types if malloc is not used (e.g. a stack-allocated buffer). Always use malloc/realloc for void* generic containers since the standard guarantees those return maximally aligned memory.
2c. X-Macros — typed code generation from a single list
X-Macros let you maintain a single canonical list (e.g. all error codes, all fields of a struct, all supported types) and expand it differently in different contexts.
/* Define the list once. Each entry is an X(type, name, default). */#define CONFIG_FIELDS \ X(int, width, 800) \ X(int, height, 600) \ X(float, scale, 1.0f) \ X(int, fps, 60)/* 1. Generate the struct */typedef struct {
#define X(T, name, def) T name;
CONFIG_FIELDS
#undef X
} Config;
/* 2. Generate a default-init function */voidconfig_defaults(Config* c) {
#define X(T, name, def) c->name = (def);
CONFIG_FIELDS
#undef X
}
/* 3. Generate a print function */voidconfig_print(const Config* c) {
#define X(T, name, def) printf(#name " = " _Generic((c->name), \ int: "%d\n", \ float: "%.2f\n"), \ c->name);
CONFIG_FIELDS
#undef X
}
int main() {
Config cfg;
config_defaults(&cfg);
config_print(&cfg);
cfg.fps = 120;
config_print(&cfg);
}
X-Macros are the closest C comes to reflection. Adding a new field to the list automatically updates the struct, the initialiser, the printer, and every other expansion — a single edit, zero omissions. This is widely used in protocol parsers, state machines, and error code tables.
2d. C11 _Generic — compile-time type dispatch
_Generic(expr, type1: val1, type2: val2, ..., default: valn) selects one of its branches based on the static type of expr. Only the selected branch is evaluated; the rest are discarded at compile time.
Simulating overloaded math functions
#include<math.h>/* abs() dispatches to the right variant based on type */#define abs_g(x) _Generic((x), \ int: abs, \ long: labs, \ long long: llabs, \ float: fabsf, \ double: fabs, \ long double: fabsl)(x)int main() {
printf("%d\n", abs_g(-7)); // calls abs(int)
printf("%f\n", abs_g(-2.5)); // calls fabs(double)
printf("%f\n", abs_g(-2.5f)); // calls fabsf(float)
}
Type-safety enforcement
/* Prevent passing a float to an integer-only bit operation */#define SAFE_POPCOUNT(x) _Generic((x), \ unsigned int: __builtin_popcount, \ unsigned long: __builtin_popcountl, \ unsigned long long: __builtin_popcountll \)(x)/* Calling SAFE_POPCOUNT(3.14) is a compile error — no matching branch */
Combining _Generic with X-Macros for full type dispatch
C++ provides a first-class generic mechanism: templates. A template is a blueprint parameterised by one or more type or non-type parameters. The compiler instantiates the template for each unique combination of arguments it encounters, producing specialised machine code.
3a. Function templates
Basic syntax and argument deduction
template <typename T>
T my_max(const T& a, const T& b) {
return (a < b) ? b : a;
}
int main() {
auto x = my_max(4, 9); // T deduced as intauto y = my_max(1.2, 3.4); // T deduced as doubleauto z = my_max<long>(5, 8); // explicit instantiation
}
Explicit specialisation — customise for a specific type
template <typename T>
T absolute(T x) { return x < T{} ? -x : x; }
// Full specialisation for const char* (lexicographic makes no sense; return length)template <>
const char* absolute<const char*>(const char* s) {
return s; // just passthrough; specialisation changes behaviour entirely
}
int main() {
printf("%d\n", absolute(-7)); // uses primary template
printf("%s\n", absolute("hi")); // uses specialisation
}
Multiple template parameters and default arguments
template <typename Key, typename Value = int>
struct Pair {
Key key;
Value val;
};
Pair<std::string> p1; // Value defaults to int
Pair<std::string, double> p2; // explicit
3b. Class templates, partial and full specialisation
A template can itself accept a template as a parameter, enabling container-agnostic algorithms.
template <typename T, template <typename, typename> class Container>
voidprint_all(const Container<T, std::allocator<T>>& c) {
for (const auto& e : c) std::cout << e << ' ';
std::cout << '\n';
}
// Works with std::vector<int>, std::deque<int>, std::list<int>, ...
print_all(std::vector<int>{1,2,3});
print_all(std::list<int>{4,5,6});
3d. Variadic templates and fold expressions
Parameter packs
// Recursive variadic sum (C++11)template <typename T>
T sum(T x) { return x; }
template <typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
auto s = sum(1, 2, 3, 4); // 10
SFINAE — Substitution Failure Is Not An Error: when the compiler tries to instantiate an overloaded template and the substitution of template arguments into the function signature would produce an ill-formed expression, that candidate is silently discarded rather than causing a compile error. This allows writing templates that only participate in overload resolution for types that satisfy certain conditions.
std::enable_if — conditional overloads
#include<type_traits>// Only active when T is an integral typetemplate <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
double_it(T x) { return x * 2; }
// Only active for floating-pointtemplate <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
double_it(T x) { return x * 2.0; }
int main() {
double_it(5); // int version
double_it(3.14); // double version// double_it("x"); // compile error: no matching overload
}
Custom type traits
// Primary template: T is NOT iterabletemplate <typename T, typename = void>
struct is_iterable : std::false_type {};
// Partial specialisation: if T has begin() and end(), it IS iterabletemplate <typename T>
struct is_iterable<T, std::void_t<
decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())
>> : std::true_type {};
// Use it as a constrainttemplate <typename C>
std::enable_if_t<is_iterable<C>::value>
print_range(const C& c) {
for (const auto& e : c) std::cout << e << ' ';
}
static_assert(is_iterable<std::vector<int>>::value, "vector is iterable");
static_assert(!is_iterable<int>::value, "int is not iterable");
Useful standard type traits quick reference
Trait
What it checks
std::is_integral<T>
int, long, bool, etc.
std::is_floating_point<T>
float, double, long double
std::is_arithmetic<T>
integral or floating point
std::is_pointer<T>
pointer type
std::is_same<T,U>
T and U are exactly the same type
std::is_base_of<Base,Derived>
inheritance relationship
std::is_constructible<T,Args...>
T can be constructed from Args
std::is_convertible<From,To>
implicit conversion exists
std::conditional<B,T,F>::type
selects T if B is true, else F
std::remove_cv<T>::type
strips const/volatile
std::decay<T>::type
models by-value argument passing rules
3f. if constexpr (C++17)
if constexpr discards the non-taken branch at compile time. This replaces many SFINAE idioms with readable conditional code inside a single function body.
Concepts replace ad-hoc SFINAE with named, reusable, readable constraints. They produce clear error messages, can be composed, and serve as documentation for what operations a type must provide.
Defining concepts
#include<concepts>// Requires T supports operator< and operator==template <typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};
// Requires T has a .size() returning a numbertemplate <typename T>
concept Sized = requires(T c) {
{ c.size() } -> std::convertible_to<size_t>;
};
// Compose conceptstemplate <typename T>
concept SortableRange = Sized<T> && requires(T c) {
{ c.begin() };
{ c.end() };
requires Comparable<typename T::value_type>;
};
Using concepts as constraints
// Approach 1: requires clause after template parameterstemplate <typename T> requires Comparable<T>
T max_val(T a, T b) { return a < b ? b : a; }
// Approach 2: concept name directly in template parameter listtemplate <Comparable T>
T min_val(T a, T b) { return a < b ? a : b; }
// Approach 3: abbreviated function template (auto with concept)autoclamp(Comparable auto v, Comparable auto lo, Comparable auto hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
// Approach 4: requires expression inlinetemplate <typename T>
requires (std::is_arithmetic_v<T> && sizeof(T) >= 4)
T safe_div(T a, T b) { return b ? a / b : T{0}; }
CRTP achieves static polymorphism: a base class template is parameterised by its own derived class, allowing the base to call derived-class methods through a template cast — no virtual table, no virtual dispatch, no heap allocation, full inlining.
Pattern structure
template <typename Derived>
class Base {
public:
voidinterface() {
// Cast this to Derived* and call the actual implementation.// No virtual dispatch — resolved at compile time.static_cast<Derived*>(this)->implementation();
}
};
class Concrete : public Base<Concrete> {
public:
voidimplementation() { std::cout << "Concrete::implementation\n"; }
};
Real use: mixin — add operators automatically
// Provide !=, >, <=, >= for free once a class defines == and <template <typename T>
struct Comparable {
booloperator!=(const T& o) const { return !(static_cast<const T&>(*this) == o); }
booloperator> (const T& o) const { return o < static_cast<const T&>(*this); }
booloperator<=(const T& o) const { return !(static_cast<const T&>(*this) > o); }
booloperator>=(const T& o) const { return !(static_cast<const T&>(*this) < o); }
};
struct Point : Comparable<Point> {
int x, y;
booloperator==(const Point& o) const { return x==o.x && y==o.y; }
booloperator< (const Point& o) const { return x<o.x || (x==o.x && y<o.y); }
// Gets !=, >, <=, >= for free via CRTP
};
CRTP for static interface enforcement
template <typename Derived>
struct IAnimal {
voidspeak() { static_cast<Derived*>(this)->speak_impl(); }
voidmove() { static_cast<Derived*>(this)->move_impl(); }
// compile error if Derived doesn't define speak_impl() or move_impl()
};
struct Dog : IAnimal<Dog> {
voidspeak_impl() { puts("Woof"); }
voidmove_impl() { puts("runs"); }
};
template <typename A>
voidmake_noise(IAnimal<A>& a) { a.speak(); a.move(); } // zero virtual overhead
3i. Policy-based design
Instead of hard-coding behaviour (how to compare, how to allocate, how to log), inject it as a template parameter — a policy. The host class composes policies at compile time using multiple inheritance or composition.
/* SortedList parameterised over a comparison policy */struct Ascending { template<typename T> boolless(T a, T b) { return a < b; } };
struct Descending { template<typename T> boolless(T a, T b) { return a > b; } };
template <typename T, typename CmpPolicy = Ascending>
class SortedList : private CmpPolicy {
std::vector<T> data_;
public:
voidinsert(const T& v) {
auto it = std::lower_bound(data_.begin(), data_.end(), v,
[this](T a, T b) { return CmpPolicy::less(a, b); });
data_.insert(it, v);
}
const std::vector<T>& data() const { return data_; }
};
SortedList<int> asc; // ascending order (default)
SortedList<int, Descending> dsc; // descending order
Policy-based design is how the C++ standard library itself is architected: std::allocator is an allocator policy for containers, std::less is a comparator policy for maps and sets, std::char_traits is a character trait policy for strings.
4. Template Metaprogramming (TMP)
Templates are a Turing-complete compile-time computation mechanism. TMP moves work from runtime to compile time: computations happen during compilation, with no runtime overhead.
Compile-time factorial
// Recursive template: compute N! at compile timetemplate <size_t N>
struct Factorial {
static constexpr size_t value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> { static constexpr size_t value = 1; };
static_assert(Factorial<5>::value == 120); // checked at compile time// Modern style: constexpr function (C++14, preferred)constexpr size_t factorial(size_t n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
static_assert(factorial(6) == 720);
Compile-time type list and operations
// A heterogeneous type listtemplate <typename... Types> struct TypeList {};
// Count the number of types in a listtemplate <typename List> struct Length;
template <typename... Ts>
struct Length<TypeList<Ts...>> {
static constexpr size_t value = sizeof...(Ts);
};
// Find if a type appears in a listtemplate <typename T, typename List> struct Contains;
template <typename T>
struct Contains<T, TypeList<>> : std::false_type {};
template <typename T, typename Head, typename... Tail>
struct Contains<T, TypeList<Head, Tail...>>
: std::conditional_t<std::is_same_v<T, Head>,
std::true_type,
Contains<T, TypeList<Tail...>>> {};
using MyTypes = TypeList<int, double, std::string>;
static_assert(Length<MyTypes>::value == 3);
static_assert(Contains<double, MyTypes>::value);
static_assert(!Contains<float, MyTypes>::value);
std::conditional — compile-time ternary for types
#include<type_traits>#include<cstdint>// Choose the smallest integer type that fits N bitstemplate <size_t Bits>
using SmallestInt =
std::conditional_t<Bits <= 8, uint8_t,
std::conditional_t<Bits <= 16, uint16_t,
std::conditional_t<Bits <= 32, uint32_t,
uint64_t>>>;
static_assert(std::is_same_v<SmallestInt<5>, uint8_t>);
static_assert(std::is_same_v<SmallestInt<12>, uint16_t>);
static_assert(std::is_same_v<SmallestInt<20>, uint32_t>);
Pure C, runtime-polymorphic containers (e.g. plugin system)
void* + element size + function pointer table
C project, type dispatch in a macro
_Generic for clean per-type dispatch without casts
Large tables of repetitive definitions in C
X-Macros to maintain one source of truth
C++ generic algorithm over STL containers
Constrained function template with concept (C++20) or SFINAE (C++17)
C++ reusable container with optional allocator
Class template with allocator policy parameter
C++ static interface (avoid vtable overhead)
CRTP
C++ algorithm that varies per type at compile time
if constexpr + type traits
Enforcing algorithm requirements clearly (C++20+)
Named concepts + requires clauses
Fixed-size stack-allocated container
Class template with non-type size parameter (FixedArray<T,N>)
Rule of thumb: if the project is C, use void* for runtime flexibility and macros for type-safe static variants — document contracts meticulously. If the project is C++17, use if constexpr and enable_if. If C++20 is available, use concepts: they are the language's intended answer to the type-requirement problem and make libraries dramatically easier to use correctly.
Template bloat: each template instantiation generates separate machine code. For a function template instantiated over 20 different types, you get 20 copies in the binary. If binary size matters (embedded systems), prefer void*-style runtime generics or, in C++, write a non-template implementation taking void* + size and wrap it with a thin type-safe template facade.
Header-only constraint: C++ function and class template definitions must be visible at the point of instantiation — typically the call site. This forces most template code to live in headers. Use extern template declarations to suppress redundant instantiations across translation units, or use explicit instantiation definitions in a single .cpp file if the set of used types is known in advance.
Practice Tasks
Try each problem on your own first. The C tasks need only a C11-capable compiler; the C++ tasks need C++14 or later.
1[C] Write a SQUARE macro.
Write a macro SQUARE(x) that returns x * x. Then test: what does SQUARE(1 + 2) produce if you forget parentheses? Fix it so it gives 9.
2[C] _Generic type-name dispatcher.
Write a macro TYPE_NAME(x) using _Generic that returns the string "int", "double", "float", or "other" depending on the type of x. Print the result for values 42, 3.14, 2.5f, and 'A'.
#include<stdio.h>#define TYPE_NAME(x) _Generic((x), \ int: "int", \ double: "double", \ float: "float", \ default:"other")int main() {
printf("%s\n", TYPE_NAME(42)); // int
printf("%s\n", TYPE_NAME(3.14)); // double
printf("%s\n", TYPE_NAME(2.5f)); // float
printf("%s\n", TYPE_NAME('A')); // other (char promotes to int in _Generic)
}
3[C++] Function template — clamp.
Write a template function clamp(val, lo, hi) that returns val if it is between lo and hi, lo if it is too small, and hi if it is too large. It must work for int, double, and any type that supports <.
4[C++] Class template — Box<T>.
Write a class template Box<T> that stores a single value of any type. It should have a constructor that takes the initial value, a get() method that returns the value, and a set(val) method that replaces it. Test with int and std::string.
5[C++] Function template — contains.
Write a function template contains(container, value) that returns true if value is found anywhere in container. It must work with std::vector<int>, std::vector<std::string>, and any container that supports range-for.