C/C++ Patterns
Source: https://cs61.seas.harvard.edu/site/2025/Patterns/
Silence Intentional Unused Variables
Section titled “Silence Intentional Unused Variables”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() {}Sized Integer Types
Section titled “Sized Integer Types”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 bysizeof.ssize_t: signed counterpart with the same width assize_t.ptrdiff_t: type of pointer subtraction, as inp2 - 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.
Linked List Basics
Section titled “Linked List Basics”Terminology:
node: one item in the list.head: first node, ornullptrwhen empty.tail: last node, ornullptrwhen 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).
Doubly Linked List With Head
Section titled “Doubly Linked List With Head”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.
Doubly Linked List With Head And Tail
Section titled “Doubly Linked List With Head And Tail”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.
Erasing While Iterating
Section titled “Erasing While Iterating”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.
Growable Arrays: Size And Capacity
Section titled “Growable Arrays: Size And Capacity”Maintain:
size: number of initialized elements.capacity: number of allocated slots.
Invariant:
size <= capacityPush-back rule:
- If
size < capacity, store the new element. - If
size == capacity, grow first. - 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.