Skip to main content

Generic Values & the Ownership Seam

intermediate17 min readLesson 115 of 148

void* makes a container generic โ€” and makes 'who frees this?' a design decision you must publish.

void* is a promise, not a type

typedef struct HNode {
    const char   *key;      /* borrowed: caller keeps it alive */
    void         *value;    /* OWNERSHIP: decided by the table's policy */
    struct HNode *next;
} HNode;

The table cannot know what value points at. That is the point โ€” and the danger. Every generic container must publish an ownership policy, and the clean way to make it explicit is a callback:

typedef void (*FreeFn)(void *value);
/* destroy_frees: if true, hmap_destroy calls free_value on every value
   still stored; if false, values are left for their owner. */
void hmap_destroy(HMap *m, int destroy_frees, FreeFn free_value);

Now the contract is checkable at the call site: a table of malloc'd buffers passes 1, free; a table of borrowed strings passes 0, NULL. The bug class this kills: double frees when two containers both "clean up" the same values, and leaks when none does.

Keys are almost always borrowed

Values may be owned; keys should be borrowed โ€” the caller's string lives in the caller's storage, the table only compares addresses of characters. If the table must own keys (the caller's buffer dies), it must strdup them โ€” and then it owns two things, and the policy multiplies. Borrowed keys keep one rule: the key outlives the entry.

The iteration contract

Any table that can be walked (hmap_foreach) must publish what mutations during iteration do. The safe rule: you may replace the current entry's value; you may not insert or remove while iterating. Enforce it or document the undefined behavior โ€” silent corruption is the alternative.

Now practice

Chaining & Generic Values GymSeparate-chaining map with generic void* values and a published ownership policy.1 challenge ยท ยท ~28 min