Tutorial - Value versus Move Semantics
In this tutorial, we explore Terra’s resource management system through the lens of value and move semantics. The default approach to resource management is to move resources when they are passed by value to a function or used in an assignment. Value semantics - where resources behave more like regular values - is simply achieved by implementing a __copy
method where you, as the programmer, are in control of how values are copied.
We’ll implement basic data structures to demonstrate how managed types integrate seamlessly, showcasing the power and simplicity of ownership transfer in Terra.
We’ll build the following managed types:
DynamicStack
: A dynamically sized container that allocates memory on the heap. It offers element access, push and pop methods, and automatically reallocates resources when capacity is exceeded. We'll add an option to compile a__copy
method that performs a deepcopy of the resource.DynamicVector
: A dynamically sized container that allocates a single chunk of heap memory without reallocation. It provides element access and supports a user-defined cast from aDynamicStack
for resource transfer. We'll add an option to compile a__copy
method that performs a deepcopy of the resource.VectorPair
: An aggregate type combining two Dynamic Vectors, with element access to its paired components.
Our focus is on the two ways of ownership transfer:
- Move semantics: how resources are transferred efficiently by default, ensuring single ownership without unnecessary copying.
- Value semantics: How to make a type copyable by implementing a
__copy
method
Let’s dive in and see these concepts in action!
Tutorial setup
You can either download the tutorial files here or follow along. We'll consider the following directory structure:
/tutorial-move-semantics/
├── libtutorial.t
├── tutorial.t
└── utils.t
First we'll write a small library for logging and checking asserts. Put the code below in a file utils.t
. Note how easy it is to simply hijack C-functions you are familiar with.
The implementation of the dynamic stack, vector and vector pair will be added to a libtutorial.t
file.
Implementation of a dynamic stack
Let’s kick off with DynamicStack
. This dynamic, heap-allocated container supports element access via stack(i)
, along with push and pop methods. It automatically reallocates when capacity is exceeded. By defining only __init
and __dtor
, we leverage Terra’s auto-generated __move
for efficient resource transfers, enforcing move-only behavior without copying—a perfect entry point to understanding Terra’s default ownership and resource management. We'll conditionally compile a __copy
method that enables copyability like ordinary values.
Some general remarks are:
- Library Support: Load
terralibext
for memory management and C’sstdlib
viaterralib.includec("stdlib.h")
. - The Lua function
terralib.memoize
enables caching of the definitionDynamicStack(T)
for element typeT
. - Static Methods:
Stack.metamethods.__getmethod
simplifies static method access, enabling calls likeStack.new
. - Element Access:
Stack.metamethods.__apply
, a macro, enablesstack(i)
for both get/set access.
Memory Management Notes
- C Integration: Uses
malloc
,realloc
, andfree
from C’sstdlib
for heap management. - Move Semantics:
push
andpop
use__move__
to transfer resources via a type’s__move
method (if defined), avoiding copies. - Implementation: Implements
__init
and__dtor
;__move
is auto-generated, making the stack movable but not copyable by default.
Important to understand is that the __copy
method is conditionally compiled. When copyable = false
the default behavior is to move data when passing by value to functions. When copyable = true
the __copy
method is compiled which enables deepcopying of data when passing arguments by value to functions.
Implementation of a dynamic vector
Next, we’ll implement DynamicVector
, a fixed-size, heap-allocated container. Unlike DynamicStack
, it doesn’t reallocate, maintaining a single memory chunk. It supports element access with vector(i)
and introduces a user-defined cast from DynamicStack
to transfer resources efficiently. An auto-generated __move
ensures move-only behavior by default. An optional __copy
method makes the type copyable.
The same general remarks apply as for the DynamicStack
implementation. C's malloc
, realloc
and free
are used to allocate, reallocate, and deallocate resources, respectively.
Specific notes on memory management are:
- Move Semantics: The
__cast
metamethod reinterpretes a reference to a stack as a reference to a vector. Paired with an auto-generated move method__move :: {&Vector, &Vector} -> {}
it enables moving from a stack into a vector object. - Implementation: Implements
__init
and__dtor
;__move
is auto-generated, ensuring movability without copyability by default.
As for the stack, conditional compilation of a __copy
method enables deepcopying of vectors when passing to functions by value.
Implementation of VectorPair
The VectorPair
is an aggregate datastructure of two DynamicVector
's. Since DynamicVector
is a managed type, VectorPair
is too. It's __init
, __move
, and __dtor
method will be auto-generated.
In particular note the __move__
directives in the following line:
The __move__
directive tells the compiler to use the __move
method rather than a __copy
(if it is implemented), avoiding any potential copies.
Example use-case
Consider next the following application code where the data structures are combined. We'll highlight where moves or copies are taking place