At the dawn of programming, C reigned supreme. C offered three memory allocation methods: static, automatic, and dynamic. While static variables, akin to constants within the source code, are straightforward due to their fixed size, automatic allocation, resembling stack allocation, allocates memory upon entering a lexical block and releases it upon exit. Notably, before C99, automatic variables needed pre-determined sizes at compile time, pushing structures like strings, lists, and maps to the heap, residing in dynamic memory.

Dynamic memory management relied on the programmer’s explicit instructions using malloc, realloc, calloc, and free. While the first two lacked initialization, potentially leaving remnants in memory, all but free could fail, returning a null pointer. Accessing this null pointer spelled disaster – the best outcome being a program crash and the worst, a ticking time bomb of garbled data.
This manual approach burdened programmers with upholding invariants to prevent catastrophic failures. Each variable access demanded a preceding malloc, success verification, and a corresponding free. Mismatches resulted in memory leaks or crashes. Accessing freed variables was forbidden. Let’s illustrate this precarious dance:
| |
| |
This seemingly innocuous code harbors an anti-pattern and a questionable practice. Hardcoding byte counts is discouraged; employing the sizeof function is preferred. Similarly, allocating memory for the character array twice, accounting for null-termination, proves inefficient. A more refined approach would involve a dynamic string buffer, permitting size adjustments.
RAII: The Dawn of a New Era
Managing memory manually was, to put it mildly, unpleasant. In the mid-1980s, Bjarne Stroustrup, the mastermind behind C++, introduced a revolutionary concept: Resource Acquisition Is Initialization (RAII). Its core tenets were: objects could possess constructors and destructors automatically invoked by the compiler, providing a streamlined memory management approach, and this technique extended beyond memory to encompass other resources.

Consequently, our previous C++ example transformed into an elegant solution:
| |
| |
Memory management vanished! Object construction, method invocation, and automatic destruction upon function exit became effortless. However, this simplicity concealed potential complexities, as demonstrated below:
| |
| |
The code functions flawlessly, populating and returning the vector of lines. However, efficiency-minded programmers would notice a potential bottleneck. Value semantics dictate copying the vector before its destruction during the return, an expensive operation for substantial data.
This isn’t strictly true in modern C++ anymore. C++11 introduced the notion of move semantics, in which the origin is left in a valid (so that it can still be properly destroyed) but unspecified state. Return calls are a very easy case for the compiler to optimize to move semantics, as it knows the origin will be destroyed shortly before any further access. However, the purpose of the example is to demonstrate why people invented a whole bunch of garbage-collected languages in the late 80s and early 90s, and in those times C++ move semantics were not available.
Seeking optimization, returning a pointer seems tempting. Despite syntactic nuances, the code remains conceptually similar:
Actually, vector is a value handle: a relatively small structure containing pointers to items on the heap. Strictly speaking, it’s not a problem to simply return the vector. The example would work better if it were a large array being returned. As attempting to read a file into a pre-allocated array would be nonsensical, we use the vector instead. Just pretend it’s an impractically large data structure, please.
| |
| |
Disaster strikes! With lines as a pointer, the stack’s ephemeral nature becomes evident. The vector’s destruction upon exiting its scope leaves the pointer dangling, resulting in a segmentation fault when accessing forbidden memory. A viable solution involves migrating the variable from the stack to the heap using a new keyword. A simple modification suffices:
| |
| |
While seemingly flawless, this solution harbors a subtle flaw: memory leakage. C++ mandates manual deletion of heap-allocated pointers; otherwise, the memory becomes inaccessible after the last pointer vanishes, recoverable only by the OS upon process termination. Modern C++ advocates for unique_ptr, automatically deleting the pointed object upon scope exit. Regrettably, this feature was absent until C++11.
Rectifying this example is straightforward:
| |
However, as programs escalate in complexity, pinpointing the precise location and time for pointer deletion becomes increasingly challenging. Ownership ambiguity arises: should a function returning a pointer relinquish or retain ownership? Errors lead to memory leaks or data structure corruption as invalid pointers are dereferenced.
Enter the Garbage Collector
Garbage collection, a concept dating back to John McCarthy’s Lisp in 1959, gained traction with Smalltalk-80 in 1980. However, the 1990s witnessed its widespread adoption with languages like Haskell, Python, Lua, Java, JavaScript, Ruby, OCaml, and C#.

Garbage collection encompasses techniques to automate memory management. While available as libraries for languages like C and C++, it’s predominantly integrated into languages that mandate it. The allure lies in abstracting away memory management, freeing programmers from this burden. Our file-reading example in Python illustrates this:
| |
| |
The array of lines materializes upon assignment and returns without duplication. Garbage collection handles cleanup after it exits its scope, albeit at an indeterminate time. Interestingly, RAII for non-memory resources isn’t idiomatic in Python. While permissible (e.g., fp = open(file_name)), context managers are preferred for deterministic release.
Abstracting memory management comes at a cost. Reference counting incurs overhead for variable assignment and scope exits. Mark-and-sweep systems introduce unpredictable pauses for cleanup, known as “stop-the-world” events. Python, employing both, suffers from both drawbacks, making it unsuitable for performance-critical or real-time applications. These penalties are evident even in trivial programs:
| |
The Python version takes nearly three times longer than the C++ counterpart. While other factors contribute, garbage collection’s impact is undeniable.
Ownership: RAII’s Resurgence
Must performance and ease of programming be mutually exclusive? Not necessarily! Programming language research continues, ushering in new paradigms. Notably, the language called Rust promises Python’s ergonomics and C’s speed while rendering dangling and null pointers impossible – they simply won’t compile. How is this achieved?

The key lies in the borrow checker, a static analysis tool ensuring memory safety at compile time. However, understanding its implications requires delving into its prerequisites.
Ownership
Recall ownership in C++, denoting responsibility for variable deletion. Rust formalizes this concept, bestowing ownership of the bound resource upon every variable binding. The borrow checker enforces singular ownership, as illustrated in this snippet from the Rust Book, which fails to compile:
| |
| |
Rust’s default move semantics transfer ownership during assignment. While copy semantics are possible, they are uncommon. Consequently, after the third line, v2 assumes ownership, rendering v inaccessible. This strictness ensures a single point of scope exit for each resource, determinable at compile time. Rust leverages this to deliver on RAII’s promise, initializing and destroying resources deterministically based on scope without garbage collection or manual intervention.
Contrast this with reference counting, where pointers store the object’s address and its reference count. Destruction occurs when the count reaches zero, doubling memory usage and introducing runtime overhead for count manipulation. Rust’s ownership achieves the same goal without runtime cost, analyzing ownership and inserting destruction calls during compilation.
Borrowing
Move semantics alone would make function return types cumbersome. Returning multiple unmodified vectors would necessitate including them in the return type. Rust addresses this with borrowing. The following function borrows references to vectors, returning ownership upon completion:
| |
| |
v1 and v2 revert to their original scope after fn foo returns, facing destruction upon their containing scope’s exit.
The borrow checker enforces borrowing restrictions, concisely outlined in the Rust Book:
A borrow’s lifespan cannot exceed its owner’s scope. Additionally, only one of the following borrow types is allowed simultaneously:
One or more immutable references (
&T)Exactly one mutable reference (
&mut T)
This restriction underpins Rust’s protection against data races. Preventing concurrent mutable access guarantees deterministic behavior, eliminating issues like iterator invalidation and use-after-free errors.
The Borrow Checker in Action
Let’s revisit our file line counter, now implemented in Rust:
| |
Beyond the in-code explanations, tracing variable lifetimes is insightful. file_name and file_lines persist until main() concludes, their destructors invoked automatically. When calling read_lines_from_file, file_name is immutably borrowed. Within read_lines_from_file, buffer follows suit, destroyed upon scope exit. lines, however, endures, returned to main().
Rust’s expression-based nature dictates that the last expression in a function, without a semicolon, serves as the return value. Return values, intended to outlive the function, receive special treatment. Due to move semantics, transferring ownership from Ok(lines) to Ok(file_lines) requires no copying; the compiler merely adjusts the pointer.
“The True Power of RAII Reveals Itself in the End”
Manual memory management, a programmer’s bane, has fueled the quest for automated solutions. RAII, while promising, faltered in C++ when handling heap-allocated objects without awkward workarounds. Consequently, the 1990s witnessed a surge in garbage-collected languages, prioritizing programmer convenience over performance.

However, language design evolves. Rust, armed with ownership and borrowing, reconciles RAII’s scope-based approach with garbage collection’s memory safety. It achieves this without the need for stop-the-world pauses, offering unparalleled safety guarantees. This is the future of systems programming. After all, “to err is human, but compilers never forget.”