Pointer values and allocation
*T has one source-level meaning: a nullable, aliasing reference to a location holding a value of type
T. new T creates a heap-owned location and yields that pointer. &place forms an address or borrow of
an already-existing location. This page specifies those operations and the CLR representations used to carry
an address when it does not need, or can no longer remain, a transient managed borrow.
Grammar
Section titled “Grammar”Type = "*" Type | NamedType | TupleType | FuncPtrType .NewExpr = "new" TypeName ( "{" [ FieldInitList ] "}" | "(" [ ArgList ] ")" ) .AddressOfExpr = "&" Expr .* nests according to Type, so *int and *Node are ordinary pointer types. A program shall not form
*Class: a class already carries reference identity and *Class is ES2003.
Pointers to value structs and primitives are valid. new requires a value struct; new on another
type is ES2144.
Source semantics
Section titled “Source semantics”A *T may be nil, stored in a field or collection, returned from a function, and shared among callers.
Member access automatically dereferences its target, so a pointer reads like the value it names:
struct Node { value: int, next: *Node }
func push(head: *Node, value: int) -> *Node = new Node { value: value, next: head }
func sum(head: *Node) -> int { var total = 0 var current = head while current != nil { total += current.value current = current.next } return total}current.value is an automatic dereference. There is no source spelling equivalent to C’s
(*current).value, and there is no unmanaged pointer arithmetic. A pointer is to a location, not a
separately observable object identity for the pointed-to value.
Values, locations, and pointer values
Section titled “Values, locations, and pointer values”The type and memory model use three deliberately separate ideas:
| Thing | Example | Meaning |
|---|---|---|
| value | Cell { value: 40 } | one Cell value, copied by ordinary value assignment and argument passing |
| location | var cell = Cell { value: 40 } | a named, addressable place currently holding one Cell value |
| pointer value | var pointer = &cell | a *Cell address/borrow that aliases that particular location |
The name pointer in the third row is itself just a normal local binding which stores a pointer value. It
does not make cell a pointer, does not create a second Cell, and does not add another level of source
indirection to cell. The following two writes reach the same location:
struct Cell { var value: int }
func addressAliasesExistingLocation() -> int { var cell = Cell { value: 40 } var pointer = &cell
pointer.value += 1 cell.value += 1 return pointer.value // 42}Source reads and writes through a non-null *T automatically dereference. That makes pointer.value read
pleasantly like a Cell access, but the type is still *Cell: it may be nil, may be copied as an alias,
and carries location-sharing semantics rather than Cell copy semantics. & inherits the mutability contract
of the place addressed: it can borrow a writable var location for mutation, or a readonly let location
for observation only.
An address-of operand must be an addressable place, not merely an expression that happens to produce a
T. var locals are writable places; let locals are readonly-addressable places; a bare typed local
(cell: Cell = expression) is mutable by default but intentionally has no addressable representation.
Properties participate only when their loca or mut contract supplies a location. An array element
&arr[i] is a place in its own right, emitted as ldelema: the CLR has no read-only array, so an element
is always writable storage, and a let binding of the array fixes the reference rather than the contents.
Because an element is heap-rooted, a pointer to one is not the non-escaping ref T form and may outlive
the frame that formed it. These rules keep addressability a deliberate representation capability rather
than an accidental consequence of mutability.
Existing locations versus fresh locations
Section titled “Existing locations versus fresh locations”& and new both produce *T, but they answer opposite questions:
| Form | Question answered | Result |
|---|---|---|
&place | “Where is this value already stored?” | an address/borrow of the existing location named by place |
new T { ... } | “Heap-allocate a new T and give me its *T.” | a heap-owned pointer to fresh, independent storage initialized with T |
struct Cell { var value: int }
func allocationIsIndependentFromAddressOf() -> int { var cell = Cell { value: 40 } var alias = &cell var fresh: *Cell = new Cell { value: 40 }
alias.value += 1 fresh.value += 2 return cell.value * 100 + fresh.value // 4142}alias and cell observe one location. fresh observes another location, even though its initial value has
the same shape. new is therefore not shorthand for & and & is not a delayed spelling of new.
new is the source-level heap-allocation operation; & is the source-level identity-preserving
address/borrow operation.
This distinction is semantic, not a promise about a particular register, stack slot, or object layout. A
program should choose &cell because another operation must alias or borrow cell, and choose
new Cell { ... } because it is allocating a fresh, pointer-owned value. The latter has a fixed heap
allocation meaning; the former intentionally says nothing about the physical residence of cell.
Address-of representation
Section titled “Address-of representation”new T always allocates its __Ptr_T heap carrier. By contrast, &place begins as an address/borrow of an
existing location. The compiler chooses the following carrier for that address according to its lifetime:
| Address-of use | CLR representation | Consequence |
|---|---|---|
| escapes its frame; is captured, returned, stored, or nullable | __Ptr_T, a heap reference cell | allocation; the cell owns the shared location |
| provably does not escape and is non-nullable | managed pointer T& (ref T) | aliases existing storage without allocation |
The compiler shall insert conversions wherever those carriers meet. This is why an address of a local can
flow to a *T parameter and a returned *T can still outlive its creator: the former can be a managed
borrow, while the latter uses the heap cell. A program cannot test which carrier was selected for &, or
make its behavior depend on it.
Managed pointers are GC-tracked. They cannot dangle and cannot escape the frame that owns their storage; the escape analysis is what enforces that restriction. The heap-cell form is ordinary GC-reachable state and remains alive while a holder can reach it. A compiler path that fails to realize a managed alias before it reaches a durable boundary must report ES2030 at the source location; it must not defer the failure to malformed IL or a run-time exception.
Address-of may become durable
Section titled “Address-of may become durable”&cell does not request allocation, but its borrow can be made durable in the source language. A *T is
first-class, so an address of a local may be returned, captured, stored, or passed through a durable API. In
that case the compiler promotes the same location to its durable heap carrier; it does not copy the
current Cell into an unrelated allocation.
struct Cell { var value: int }
func makeCell() -> *Cell { var cell = Cell { value: 40 } return &cell}
func usePromotedAddress() -> int { let pointer = makeCell() pointer.value += 2 return pointer.value // 42}The local cell names the location at the point &cell is formed. The returned pointer keeps that location
alive through a compiler-selected durable carrier. This is why “address of a local” is the right source
description, while “stack address” is not a stable semantic description.
Suspension preserves location identity
Section titled “Suspension preserves location identity”await splits one source activation into an initial execution and one or more state-machine resumptions.
If a local location is both live after a suspension and participates in a durable pointer operation, the
state machine stores that local’s shared __Ptr_T carrier, not a copied T. This includes an awaited call
whose *T parameter is itself durable. The callee, the caller after resumption, captured holders, and
generic containers must therefore all observe the same location:
struct Cell { var value: int }
func bump(cell: *Cell) -> Task<int> { await Task.Delay(1) cell.value += 1 return cell.value}
func observe() -> Task<int> { var cell = Cell { value: 41 } let result = await bump(&cell) return cell.value * 100 + result // 4242}The representation remains unobservable: observe has not changed the type or meaning of cell, and a
non-escaping address use still remains eligible for the allocation-free managed-pointer representation.
Allocation versus address-of
Section titled “Allocation versus address-of”new heap-allocates a location that did not exist and produces its *T. & borrows or takes the address
of a location that already exists:
struct Data { var value: int }
func change(data: *Data) { data.value = 99 }
func existingLocationAlias() -> int { var value = Data { value: 10 } change(&value) return value.value}
func heapCell() -> int { let value = new Data { value: 10 } change(value) return value.value}The first call may emit ldloca and pass Data&, with no allocation. The second allocates __Ptr_Data
and passes its underlying location. Both calls satisfy the same source signature and observe the same
mutation. new is the only allocation expression; & itself never requests allocation, although an
address that later escapes must be represented safely as a heap cell.
Type-system boundaries
Section titled “Type-system boundaries”*T is a type constructor over value-disciplined T; it is not an unsafe native address type and it does
not turn T itself into a reference type. The distinction matters at several boundaries:
Tis an ordinary value. AssigningCellto anotherCellcopies its value.T?is an optional value. It records absence but does not create a shared mutable location.*Tis an optional alias to a location holdingT; copying the pointer copies the alias. Operations through an address formed with&remain subject to the addressed place’s writable or readonly contract.class Calready has CLR reference identity.*Cis forbidden; class-valued property locations are an opaque property protocol, not a loophole for source pointers to classes.List<*T>, a closure capture, a returned pointer, and an async state machine are durable contexts. They preserve*Tidentity by selecting a heap cell when a managed byref would be illegal.
For example, var other = pointer copies the pointer value and therefore aliases the same Cell; it does
not copy the cell. Conversely, var other = cell copies the Cell value and gives other independent
storage. This difference is the practical reason the type spelling is visible in APIs.
The CLR’s ref/in/out parameter machinery is an implementation route for some *T operations, not a
second source-level pointer family. By-ref calls and method sets
specifies those call boundaries; Memory model specifies copying, initialization, and
visibility; properties and initialization
specifies the explicit property-location boundary.
Generic boundaries
Section titled “Generic boundaries”The CLR forbids a managed pointer as a generic type argument. Therefore *T in a generic-argument,
field, collection, capture, or return context normalizes to the heap-cell representation. This is an
implementation constraint, not a second source type: an E# generic API still declares and receives *T.
See also
Section titled “See also”- Pointers & by-ref — the complete pointer reference.
- By-ref calls and method sets — parameter and call-site forms.
- Memory model — copy, aliasing, and initialization rules.