Generics in C and C++

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 repeated int max_int (int a, int b) { return a > b ? a : b; } double max_double(double a, double b) { return a > b ? a : b; } float max_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 int double y = MAX(1.5, 2.7); // works for double
// C++ — function template: type-safe, one definition, any comparable type template <typename T> T max_val(T a, T b) { return a > b ? a : b; } int x = max_val(3, 10); // compiler generates an int version double 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.

Topics of Discussion

  1. What generic programming means — theory
  2. Generics in C
  3. Generics in C++
  4. Template metaprogramming
  5. Comparison: C vs C++
  6. Practical guidance

1. What Generic Programming Means — Theory

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

ModelResolution timeMechanism (C++)Mechanism (C)Cost
Parametric (generics)Compile-timeTemplates / conceptsMacros, X-macrosZero (code duplication by compiler)
Subtype (OOP)RuntimeVirtual functionsFunction pointers in structsvtable indirection, cache misses
Ad-hoc (overloading)Compile-timeFunction overloading_GenericZero

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.

Type-safe typed macro containers (poor-man's templates)

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.

/* vec.h — generic growable array via macro instantiation */ #define DEFINE_VEC(T, Name) \ typedef struct { T* data; size_t len, cap; } Name; \ \ static inline void Name##_init(Name* v) { \ v->data = NULL; v->len = 0; v->cap = 0; \ } \ \ static inline void Name##_push(Name* v, T val) { \ if (v->len == v->cap) { \ v->cap = v->cap ? v->cap * 2 : 8; \ v->data = realloc(v->data, v->cap * sizeof(T)); \ } \ v->data[v->len++] = val; \ } \ \ static inline T Name##_get(const Name* v, size_t i) { \ return v->data[i]; \ } \ \ static inline void Name##_free(Name* v) { free(v->data); } /* Instantiate once per type needed */ DEFINE_VEC(int, IntVec) DEFINE_VEC(double, DblVec) int main() { IntVec iv; IntVec_init(&iv); IntVec_push(&iv, 10); IntVec_push(&iv, 20); printf("%d\n", IntVec_get(&iv, 1)); // 20 IntVec_free(&iv); }
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> int cmp_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 } int cmp_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; void gs_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); } void gs_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++; } int gs_pop(GenStack* s, void* out) { if (s->top == 0) return 0; s->top--; memcpy(out, s->buf + s->top * s->esize, s->esize); return 1; } void gs_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 */ void config_defaults(Config* c) { #define X(T, name, def) c->name = (def); CONFIG_FIELDS #undef X } /* 3. Generate a print function */ void config_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

/* printf format string dispatcher */ #define FMT(x) _Generic((x), \ char: "%c", \ int: "%d", \ unsigned int: "%u", \ long: "%ld", \ float: "%f", \ double: "%lf", \ char*: "%s", \ const char*: "%s", \ default: "%p") #define PRINT(x) printf(FMT(x), (x)) int main() { PRINT(42); // %d PRINT(3.14); // %lf PRINT("hello"); // %s }

3. Generics in C++

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 int auto y = my_max(1.2, 3.4); // T deduced as double auto 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

Generic Stack

#include <vector> #include <stdexcept> template <typename T> class Stack { std::vector<T> data_; public: void push(const T& v) { data_.push_back(v); } void push(T&& v) { data_.push_back(std::move(v)); } void pop() { data_.pop_back(); } T& top() { return data_.back(); } const T& top() const { return data_.back(); } bool empty() const { return data_.empty(); } size_t size() const { return data_.size(); } };

Full specialisation — entirely custom implementation for one type

// Primary template template <typename T> class Serialiser { public: std::string encode(const T& v) { return std::to_string(v); } }; // Full specialisation for bool template <> class Serialiser<bool> { public: std::string encode(bool v) { return v ? "true" : "false"; } };

Partial specialisation — customise for a family of types

// Primary template: generic ToString template <typename T> struct ToString { static std::string get(const T& v) { return std::to_string(v); } }; // Partial specialisation: for ANY pointer type T* template <typename T> struct ToString<T*> { static std::string get(T* p) { if (!p) return "null"; return "ptr:" + std::to_string(reinterpret_cast<uintptr_t>(p)); } }; // Partial specialisation: for std::vector<T> for any T template <typename T> struct ToString<std::vector<T>> { static std::string get(const std::vector<T>& v) { std::string s = "["; for (size_t i = 0; i < v.size(); ++i) s += (i ? "," : "") + ToString<T>::get(v[i]); return s + "]"; } };

3c. Non-type and template-template parameters

Non-type parameters

Template parameters can be compile-time integer values, not just types. This allows stack-allocated fixed-size containers with no heap allocation.

template <typename T, size_t N> class FixedArray { T data_[N]; size_t len_ = 0; public: void push(const T& v) { if (len_ < N) data_[len_++] = v; } T& operator[](size_t i) { return data_[i]; } const T& operator[](size_t i) const { return data_[i]; } size_t size() const { return len_; } constexpr size_t capacity() const { return N; } }; FixedArray<int, 16> buf; // 16 ints on the stack, no heap

Template-template parameters

A template can itself accept a template as a parameter, enabling container-agnostic algorithms.

template <typename T, template <typename, typename> class Container> void print_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

Fold expressions (C++17) — no recursion needed

template <typename... Args> auto sum(Args... args) { return (... + args); // unary left fold: ((a + b) + c) + ... } template <typename... Args> void print_all(Args... args) { ((std::cout << args << ' '), ...); // comma fold std::cout << '\n'; } template <typename... Args> bool all_positive(Args... args) { return (... && (args > 0)); // logical AND fold } sum(1, 2, 3); // 6 print_all(42, 3.14, "hello"); // 42 3.14 hello all_positive(1, 2, 3); // true

Applying a function to each element of a tuple

#include <tuple> template <typename Tuple, typename F, size_t... I> void for_each_impl(Tuple&& t, F&& f, std::index_sequence<I...>) { (f(std::get<I>(t)), ...); } template <typename Tuple, typename F> void for_each_tuple(Tuple&& t, F&& f) { constexpr size_t N = std::tuple_size_v<std::remove_reference_t<Tuple>>; for_each_impl(std::forward<Tuple>(t), std::forward<F>(f), std::make_index_sequence<N>{}); } int main() { auto tup = std::make_tuple(1, 2.5, std::string("hi")); for_each_tuple(tup, [](const auto& x) { std::cout << x << '\n'; }); }

3e. SFINAE, enable_if, and type traits

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 type template <typename T> typename std::enable_if<std::is_integral<T>::value, T>::type double_it(T x) { return x * 2; } // Only active for floating-point template <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 iterable template <typename T, typename = void> struct is_iterable : std::false_type {}; // Partial specialisation: if T has begin() and end(), it IS iterable template <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 constraint template <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

TraitWhat 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>::typeselects T if B is true, else F
std::remove_cv<T>::typestrips const/volatile
std::decay<T>::typemodels 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.

#include <type_traits> template <typename T> void describe(const T& v) { if constexpr (std::is_integral_v<T>) { std::cout << "integer: " << v << '\n'; } else if constexpr (std::is_floating_point_v<T>) { std::cout << "float: " << v << '\n'; } else if constexpr (std::is_same_v<T, std::string>) { std::cout << "string of length " << v.size() << '\n'; } else { std::cout << "unknown type\n"; } } // Compile-time recursive variadic function without overloads template <typename T, typename... Rest> void print_each(T first, Rest... rest) { std::cout << first; if constexpr (sizeof...(rest) > 0) { std::cout << ", "; print_each(rest...); } }

3g. C++20 Concepts — explicit type requirements

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 number template <typename T> concept Sized = requires(T c) { { c.size() } -> std::convertible_to<size_t>; }; // Compose concepts template <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 parameters template <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 list template <Comparable T> T min_val(T a, T b) { return a < b ? a : b; } // Approach 3: abbreviated function template (auto with concept) auto clamp(Comparable auto v, Comparable auto lo, Comparable auto hi) { return v < lo ? lo : (v > hi ? hi : v); } // Approach 4: requires expression inline template <typename T> requires (std::is_arithmetic_v<T> && sizeof(T) >= 4) T safe_div(T a, T b) { return b ? a / b : T{0}; }

Standard library concepts (C++20)

ConceptMeaning
std::integral<T>T is an integer type
std::floating_point<T>T is float, double, long double
std::same_as<T,U>T and U are the same type
std::derived_from<D,B>D is derived from B
std::convertible_to<From,To>implicit conversion From → To exists
std::equality_comparable<T>T has == and !=
std::totally_ordered<T>T supports all six comparison operators
std::copyable<T>T is copy-constructible and assignable
std::movable<T>T is move-constructible and assignable
std::regular<T>default-constructible, copyable, equality-comparable
std::invocable<F, Args...>F is callable with Args
std::ranges::range<R>R has begin() and end()

3h. CRTP — Curiously Recurring Template Pattern (static polymorphism)

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: void interface() { // 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: void implementation() { std::cout << "Concrete::implementation\n"; } };

Real use: mixin — add operators automatically

// Provide !=, >, <=, >= for free once a class defines == and < template <typename T> struct Comparable { bool operator!=(const T& o) const { return !(static_cast<const T&>(*this) == o); } bool operator> (const T& o) const { return o < static_cast<const T&>(*this); } bool operator<=(const T& o) const { return !(static_cast<const T&>(*this) > o); } bool operator>=(const T& o) const { return !(static_cast<const T&>(*this) < o); } }; struct Point : Comparable<Point> { int x, y; bool operator==(const Point& o) const { return x==o.x && y==o.y; } bool operator< (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 { void speak() { static_cast<Derived*>(this)->speak_impl(); } void move() { static_cast<Derived*>(this)->move_impl(); } // compile error if Derived doesn't define speak_impl() or move_impl() }; struct Dog : IAnimal<Dog> { void speak_impl() { puts("Woof"); } void move_impl() { puts("runs"); } }; template <typename A> void make_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> bool less(T a, T b) { return a < b; } }; struct Descending { template<typename T> bool less(T a, T b) { return a > b; } }; template <typename T, typename CmpPolicy = Ascending> class SortedList : private CmpPolicy { std::vector<T> data_; public: void insert(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 time template <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 list template <typename... Types> struct TypeList {}; // Count the number of types in a list template <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 list template <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 bits template <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>);

5. Comparison: C vs C++

AspectCC++
Primary mechanismMacros, void*, _Generic, X-MacrosTemplates, concepts, constexpr
Type safetyLargely manual; void* bypasses the type systemStrong compile-time checking, concepts enforce requirements explicitly
Error timingWrong casts detected at runtime (if at all)Type violations caught at instantiation / concept check
Error qualityCryptic macro expansion errors or silent UBReadable errors with concepts; verbose without
PerformanceFast — explicit and manual; no hidden overheadZero-cost abstractions; compiler eliminates all template machinery
Code duplicationX-Macros expand at source level (visible in object file)Template instantiation happens in compiler internals
Partial specialisationNot availableFull and partial specialisation for both functions and classes
MetaprogrammingCompile-time only via macros (no computation)Full Turing-complete TMP via templates + constexpr
Binary sizeOne copy of void* function per algorithmOne instantiation per (algorithm, type) pair — can grow
Compile speedFastSlower due to template instantiation
ABI stabilityStable (C ABI)Fragile (name mangling, inline instantiation)

Key similarities

  • Both can achieve near-identical runtime performance for the same task.
  • Both require the programmer to define contracts (what operations a type must support).
  • Both are zero-overhead at runtime when used correctly.
  • Both benefit from clearly documented generic interfaces.

Key differences

  • C++ provides native language support; C simulates generics with idioms.
  • C++ templates operate on the full type system; C void* erases it.
  • C++ can enforce interface requirements (concepts); C cannot without runtime checks.
  • C generic code is simpler to link and distribute (stable C ABI); C++ template code must often be header-only.

6. Practical Guidance

SituationRecommended approach
Pure C project, simple per-type variants neededMacro-generated typed structs/functions (DEFINE_VEC style)
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 CX-Macros to maintain one source of truth
C++ generic algorithm over STL containersConstrained function template with concept (C++20) or SFINAE (C++17)
C++ reusable container with optional allocatorClass template with allocator policy parameter
C++ static interface (avoid vtable overhead)CRTP
C++ algorithm that varies per type at compile timeif constexpr + type traits
Enforcing algorithm requirements clearly (C++20+)Named concepts + requires clauses
Fixed-size stack-allocated containerClass 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.

#include <stdio.h> // BROKEN — missing parentheses around x #define SQUARE_BAD(x) x * x // SQUARE_BAD(1+2) → 1+2*1+2 → 5 (wrong!) // CORRECT — wrap every argument and the whole expression #define SQUARE(x) ((x) * (x)) // SQUARE(1+2) → ((1+2) * (1+2)) → 9 ✓ int main() { printf("%d\n", SQUARE_BAD(1+2)); // 5 — bug printf("%d\n", SQUARE(1+2)); // 9 — correct printf("%d\n", SQUARE(5)); // 25 }

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 <.

#include <iostream> template <typename T> T clamp(T val, T lo, T hi) { if (val < lo) return lo; if (hi < val) return hi; return val; } int main() { std::cout << clamp(5, 1, 10) << "\n"; // 5 (in range) std::cout << clamp(-3, 1, 10) << "\n"; // 1 (below lo) std::cout << clamp(99, 1, 10) << "\n"; // 10 (above hi) std::cout << clamp(0.5, 0.0, 1.0) << "\n"; // 0.5 (double) }

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.

#include <iostream> #include <string> template <typename T> class Box { T value_; public: Box(T v) : value_(v) {} T get() const { return value_; } void set(const T& v) { value_ = v; } }; int main() { Box<int> intBox(42); std::cout << intBox.get() << "\n"; // 42 intBox.set(100); std::cout << intBox.get() << "\n"; // 100 Box<std::string> strBox("hello"); std::cout << strBox.get() << "\n"; // hello strBox.set("world"); std::cout << strBox.get() << "\n"; // world }

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.

#include <vector> #include <string> #include <iostream> template <typename Container, typename T> bool contains(const Container& c, const T& value) { for (const auto& elem : c) { if (elem == value) return true; } return false; } int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; std::cout << contains(nums, 3) << "\n"; // 1 (true) std::cout << contains(nums, 9) << "\n"; // 0 (false) std::vector<std::string> words = {"cat", "dog", "bird"}; std::cout << contains(words, std::string("dog")) << "\n"; // 1 std::cout << contains(words, std::string("fish")) << "\n"; // 0 }