Skip to content

C/C++ Patterns

Source: https://cs61.seas.harvard.edu/site/2025/Patterns/

Compile warning-free, but make intentional omissions explicit.

void* m61_malloc(size_t sz, const char* file, int line) {
(void) file, (void) line; // silence warnings
return malloc(sz);
}

Use this when a parameter will be used later, or is required by an interface but not used by this implementation. The (void) cast says: “I know this value is unused.”

For unused static functions in modern C++:

[[maybe_unused]] static void helper() {
}

Include:

#include <cstdint>

Common choices:

  • uint8_t, int8_t: exactly 8 bits.
  • uint16_t, int16_t: exactly 16 bits.
  • uint32_t, int32_t: exactly 32 bits.
  • uint64_t, int64_t: exactly 64 bits.
  • uintptr_t, intptr_t: integer type the same size as a pointer.
  • size_t: unsigned type for object sizes; returned by sizeof.
  • ssize_t: signed counterpart with the same width as size_t.
  • ptrdiff_t: type of pointer subtraction, as in p2 - p1.
  • off_t: file sizes and file offsets; can be larger than memory-sized types.

Rule of thumb: choose a type that matches the domain. Use size_t for memory object sizes, ptrdiff_t for pointer differences, fixed-width types when the bit width matters, and uintptr_t only when you really need pointer-sized integer behavior.

Terminology:

  • node: one item in the list.
  • head: first node, or nullptr when empty.
  • tail: last node, or nullptr when empty.
  • next: moves head-to-tail.
  • prev: moves tail-to-head.
  • trav: conventional traversal variable name.

Linked lists support O(1) insertion/deletion when you already have the relevant node, but search is usually O(N).

Supports O(1):

  • push front
  • erase known node
struct node {
node* next;
node* prev;
};
struct list {
node* head = nullptr;
};
void list_push_front(list* l, node* n) {
n->next = l->head;
n->prev = nullptr;
if (l->head) {
l->head->prev = n;
}
l->head = n;
}
void list_erase(list* l, node* n) {
if (n->next) {
n->next->prev = n->prev;
}
if (n->prev) {
n->prev->next = n->next;
} else {
l->head = n->next;
}
}

Key edge case: erasing the head needs l->head = n->next.

Supports O(1):

  • push front
  • push back
  • erase known node
struct list {
node* head = nullptr;
node* tail = nullptr;
};
void list_push_front(list* l, node* n) {
n->next = l->head;
n->prev = nullptr;
if (l->head) {
l->head->prev = n;
} else {
l->tail = n;
}
l->head = n;
}
void list_push_back(list* l, node* n) {
n->next = nullptr;
n->prev = l->tail;
if (l->tail) {
l->tail->next = n;
} else {
l->head = n;
}
l->tail = n;
}
void list_erase(list* l, node* n) {
if (n->next) {
n->next->prev = n->prev;
} else {
l->tail = n->prev;
}
if (n->prev) {
n->prev->next = n->next;
} else {
l->head = n->next;
}
}

Extra invariant: if the list has one node, that node is both head and tail. Every insertion and deletion must preserve this.

Problem: erasing an element invalidates the iterator pointing to it.

Pattern:

  • If you keep the element, increment the iterator.
  • If you erase the element, assign the iterator returned by erase.
void remove_even_items(std::map<std::string, int>& map) {
for (auto it = map.begin(); it != map.end(); ) {
if (it->second % 2 == 0) {
it = map.erase(it);
} else {
++it;
}
}
}

Do not write ++it after erase(it) unless the API specifically says that remains valid.

Maintain:

  • size: number of initialized elements.
  • capacity: number of allocated slots.

Invariant:

size <= capacity

Push-back rule:

  1. If size < capacity, store the new element.
  2. If size == capacity, grow first.
  3. Growth strategy: double capacity, with a small initial capacity such as 8.
struct vector_of_T {
T* data = nullptr;
size_t size = 0;
size_t capacity = 0;
};
T* vector_get(vector_of_T* v, size_t i) {
assert(i < v->size);
return &v->data[i];
}
void vector_push_back(vector_of_T* v, T new_element) {
if (v->size == v->capacity) {
size_t new_capacity = v->capacity ? v->capacity * 2 : 8;
v->data = (T*) realloc(v->data, sizeof(T) * new_capacity);
v->capacity = new_capacity;
}
v->data[v->size] = new_element;
++v->size;
}

Why doubling matters: growing by a fixed amount causes too many reallocations and copies. Doubling makes allocation overhead logarithmic in the maximum size reached.

In normal C++, prefer std::vector<T>. Learn this pattern to understand why std::vector is efficient and how capacity-based growth works.