2025-06-21

memory ownership and transfer semantics

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.

coordinate model

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.

allocation

describes where the memory originates:

  • static: global or static storage duration, fixed at program start
  • stack: local to a function or block, deallocated on return
  • caller: allocated by the calling code, typically on the heap
  • callee: allocated within the called function or module
  • container: owned internally by a data structure or object
  • allocator: obtained from an external allocator (e.g. pool, arena)

transfer

describes how ownership is passed between components:

  • none: no ownership is transferred
  • borrow: temporary access without ownership
  • to_caller: callee allocates and transfers ownership to the caller
  • to_callee: caller allocates and transfers ownership to the callee
  • shared: multiple parties access the memory under a shared protocol

deallocation

indicates who is responsible for freeing the memory:

  • none: memory is not freed (e.g. static, ephemeral, or borrow)
  • caller: the caller must free the memory
  • callee: the callee is responsible for cleanup
  • protocol: ownership is shared; cleanup is governed by a refcount or similar mechanism
  • auto: memory is released automatically via scope or compiler feature
  • allocator: memory is freed through a custom allocator interface

mutability

indicates whether the memory may be modified:

  • read-only: the memory must not be mutated
  • mutable: mutation is permitted

realloc

indicates whether the memory may be resized in place or replaced:

  • yes: reallocation is allowed
  • no: reallocation is forbidden

semantic categories

non-owning access

  • allocation: static or stack
  • transfer: none or borrow
  • deallocation: none
  • mutability: read-only or mutable
  • realloc: no

use cases:

  • global constants
  • stack-local borrows

caller-owned lifetime

  • allocation: caller
  • transfer: none
  • deallocation: caller

subcases:

  • immutable input:

    • mutability: read-only
    • realloc: no
  • mutable input:

    • mutability: mutable
    • realloc: no
  • growable buffer:

    • mutability: mutable

    • realloc: yes

use cases:

  • callers passing buffers
  • growable storage areas

callee-owned lifetime

  • allocation: callee
  • transfer: to_caller or none
  • deallocation: caller or callee

subcases:

  • heap allocation for return:

    • transfer: to_caller
    • deallocation: caller
  • internal retained data:

    • transfer: none

    • deallocation: callee

use cases:

  • factory-like apis
  • internal caches or resources

shared or reference-counted

  • transfer: shared
  • deallocation: protocol
  • realloc: no
  • allocation: caller, callee, container

use cases:

  • shared data structures
  • refcounted heap objects

container-opaque

  • allocation: container
  • transfer: none
  • deallocation: container

use cases:

  • vectors, sets, maps
  • abstract memory handling

allocator-mediated

  • allocation: allocator
  • deallocation: allocator
  • transfer: none, to_caller, to_callee
  • realloc: yes or no

use cases:

  • region allocators
  • memory pools

most common patterns

  • global constant: (static, none, none, read-only, no)

    • constant data stored in static memory, accessible globally without allocation or deallocation.
  • stack parameter: (stack, none, none, read-only, no)

    • temporary data passed via the stack, valid only within the caller's frame, and never freed explicitly.
  • caller buffer: (caller, none, caller, mutable, no)

    • the caller allocates and owns the memory; the callee may read or modify it but must not free or reallocate.
  • growable caller buffer: (caller, none, caller, mutable, yes)

    • like the caller buffer, but the callee may replace the pointer with a larger allocation.
  • factory return: (callee, to_caller, caller, mutable, no)

    • the callee allocates a new block (typically on the heap) and passes ownership to the caller, who must later free it.
  • shared object: (caller, shared, protocol, mutable, no)

    • memory shared between multiple components, with coordinated access and lifetime via a protocol such as reference counting.
  • 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.

less common, still useful

  • raii-like cleanup: (stack, none, auto, mutable, no)

    • memory is allocated on the stack and cleaned up automatically via scope-exit hooks or compiler extensions.
  • mutable borrow: (caller, borrow, none, mutable, no)

    • temporary, exclusive access to mutable memory, where the callee must not retain or free the pointer.
  • 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.

excluded patterns

  • physically invalid cases

    • for example, static memory with realloc: yes. static memory is fixed at program start and cannot be resized.
  • non-escaping stack/static with transfer

    • for example, (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.

example cases

caller-controlled buffer allocation

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.

  • heap variant: (caller, none, caller, mutable, no)
  • stack variant: (stack, none, none, mutable, no)
  • category: caller-owned lifetime (heap), non-owning access (stack)

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.

object-scoped references

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.

  • 5-tuple: (container, none, container, mutable, no)
  • category: container-opaque

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.

self-managed state object with internal memory

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.

  • internal allocation: (callee, none, protocol, mutable, yes)
  • external object (stack): (stack, none, none, mutable, no)
  • external object (heap): (caller, none, caller, mutable, no)
  • category: allocator-mediated

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.