a first-pass taxonomy using a 5-variable coordinate model.
this document provides a structured and comprehensive model of memory ownership and transfer semantics in low-level systems programming. by enumerating valid combinations of allocation origin, ownership transfer, deallocation responsibility, mutability, and reallocability, it defines the complete design space of practical memory-handling patterns. the framework serves as a tool for guiding api design, documentation, refactoring, and cross-language interoperability. it enables programmers to make deliberate, consistent choices about memory semantics, moving beyond implicit conventions toward explicit, verifiable contracts.
each memory handling pattern is represented as a 5-tuple:
(allocation, transfer, deallocation, mutability, realloc)
there are 720 theoretical combinations. after applying practical constraints, approximately 100 combinations remain viable.
describes where the memory originates:
describes how ownership is passed between components:
indicates who is responsible for freeing the memory:
indicates whether the memory may be modified:
indicates whether the memory may be resized in place or replaced:
use cases:
subcases:
immutable input:
mutable input:
growable buffer:
mutability: mutable
realloc: yes
use cases:
subcases:
heap allocation for return:
internal retained data:
transfer: none
deallocation: callee
use cases:
use cases:
use cases:
use cases:
global constant: (static, none, none, read-only, no)
stack parameter: (stack, none, none, read-only, no)
caller buffer: (caller, none, caller, mutable, no)
growable caller buffer: (caller, none, caller, mutable, yes)
factory return: (callee, to_caller, caller, mutable, no)
shared object: (caller, shared, protocol, mutable, no)
container-managed storage: (container, none, container, mutable, no)
memory is owned and managed entirely by a container object; users never see or manage raw allocation.
raii-like cleanup: (stack, none, auto, mutable, no)
mutable borrow: (caller, borrow, none, mutable, no)
allocator-to-caller handoff: (allocator, to_caller, allocator, mutable, yes)
memory is allocated by an external allocator, handed to the caller, and expected to be freed through the same allocator.
physically invalid cases
yes
. static memory is fixed at program start and cannot be resized.non-escaping stack/static with transfer
(stack, to_callee, ...)
. stack and static memory must not be transferred to callees that may retain or free it after the stack frame ends.compositional/multilayer ownership
for example container-of-shared-of-allocator. complex nesting of ownership (e.g. shared objects within container-managed regions using custom allocators) is excluded from this taxonomy for clarity and tractability.
a function takes a buffer allocated by the caller. the caller may use stack or heap allocation depending on context. the callee reads from or writes into the buffer but does not free or reallocate it.
(caller, none, caller, mutable, no)
(stack, none, none, mutable, no)
in this case, the caller manages the memory and can choose what memory type to use and re-use for optimized performance. the callee can focus on the core algorithm.
an object tracks dynamically allocated resources (e.g. a list of references or handles). when the object is destroyed, all associated memory is released as part of its internal cleanup logic.
(container, none, container, mutable, no)
here, memory is associated with object lifetime, which allows for the freeing of multiple memory regions as soon as they are not needed anymore. memory references are tracked with the object and dont have to be tracked separately, regardless of where the object is passed to.
a state object is allocated (either on the stack or heap) and passed to library code. internally, the callee allocates and possibly reallocates heap memory within the state. destruction is handled via a specific free function that cleans up all internal memory. stack allocation is possible for the object itself, provided the cleanup routine does not free the outer struct.
(callee, none, protocol, mutable, yes)
(stack, none, none, mutable, no)
(caller, none, caller, mutable, no)
in this case, the callee manages the memory for all needs except free, and callers don't have to worry about it except for the final free.