Перейти к содержимому

Указатели и память

В Python указателей нет — есть ссылки, и управляет ими сборщик мусора. В Mojo сборщика нет, а память кто-то должен освобождать. Обычно это делают List, String и Dict, и трогать указатели напрямую не приходится. Но иногда приходится.

Выбор между ними сводится к одному вопросу: сколько у значения владельцев.

ТипВладельцевКогда брать
Pointerноль, это просто вид на чужоесослаться на существующее значение
OwnedPointerровно одинзначение в куче с единственным хозяином
ArcPointerсколько угодноодно значение читают из нескольких мест

Ничем не владеет и ничего не освобождает: просто указывает на значение, которое живёт где-то ещё.

from std.memory import Pointer
def main():
var counter = 10
var view = Pointer(to=counter)
counter += 5
print(view[])
Результат
15

Обратите внимание: view показывает текущее значение, а не копию на момент создания. Компилятор при этом следит, чтобы указатель не пережил то, на что смотрит, — это те самые origins из главы Времена жизни.

Кладёт значение в кучу и владеет им единолично. Скопировать нельзя:

error: value of type 'OwnedPointer[Buffer]' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
Что это значит

Копия означала бы второго владельца, а весь смысл типа — что владелец один. Освободили бы дважды.

Как исправить

Передайте владение через ^ — или возьмите ArcPointer, если владельцев действительно нужно несколько.

Это прямой аналог Box из Rust и unique_ptr из C++.

Хранит счётчик ссылок: значение живёт, пока на него смотрит хоть кто-то.

from std.memory import ArcPointer
@fieldwise_init
struct Config(Movable, Deinitable):
var retries: Int
def main():
var config = ArcPointer(Config(3))
print("ссылок:", config.count())
var shared = config
print("после копии:", config.count())
print("значение:", shared[].retries)
Результат
ссылок: 1
после копии: 2
значение: 3

Счётчик — это цена. Каждое копирование и каждое уничтожение меняют его, а для многопоточности это атомарная операция. Берите ArcPointer, только когда владельцев действительно несколько.

Классический способ построить список или дерево — поле, указывающее на такую же структуру. В Mojo это получается, но не с любым указателем. Владеющие не годятся.

С OwnedPointer ошибка приходит сразу, при разборе:

error: field 'next' has non-'Deinitable' type 'OwnedPointer[Node]'
Что это значит

Чтобы уничтожить Node, нужно уметь уничтожать OwnedPointer[Node], для чего нужно уметь уничтожать Node. Круг не размыкается.

Как исправить

Сделать поле невладеющим — обычный Pointer, см. ниже.

С ArcPointer любопытнее. Само поле собирается прекрасно:

struct Node(Movable, Deinitable):
var value: Int
var next: ArcPointer[Node] # это компилируется

Проверено: size_of[Node]() даёт 16, List[Node] создаётся и уничтожается. Беда приходит, когда список нужно чем-то закончить. Нулевого указателя в Mojo нет — «ничего» выражают через Optional, и вот на нём раскладка ломается:

error: struct has recursive reference to itself

Так что владеющей рекурсии не выходит: без Optional список бесконечен, с Optional не компилируется.

Если поле только смотрит на другой узел, а память живёт отдельно, всё работает. Узлы кладём в один блок, ссылки делаем обычными Pointer:

comptime Link = Optional[Pointer[Node, MutUntrackedOrigin]]
struct Node(Movable, Deinitable):
var value: Int
var next: Link
def __init__(out self, value: Int, var next: Link):
self.value = value
self.next = next^

MutUntrackedOrigin — встроенное имя, означающее «изменяемая память, за временем жизни которой компилятор не следит». Именно такой origin получает вручную выделенный блок, и именно поэтому круг размыкается: Pointer ничем не владеет, уничтожать через него нечего.

Если сложность рекурсии не нужна, узлы складывают в List, а вместо ссылки хранят номер элемента:

@fieldwise_init
struct Item(Copyable, Movable):
var value: Int
var next: Int # -1 — конец списка
def main():
var items = List[Item]()
items.append(Item(1, 1))
items.append(Item(2, -1))
var i = 0
while i >= 0:
print(items[i].value)
i = items[i].next
Результат
1
2

Если нужен сырой блок, его выделяют через alloc с описанием раскладки:

from std.memory import Layout, dealloc
def main():
var block = alloc(Layout[Int](count=3))
var raw = block.unsafe_ptr()
for i in range(3):
raw.unsafe_store(i, i * 100)
print(raw.unsafe_load(0), raw.unsafe_load(2))
dealloc(block^)
Результат
0 200

Забыть освобождение не выйдет — компилятор не даст:

error: 'block' abandoned without being explicitly destroyed
Что это значит

Блок памяти — это владеющее значение, и просто выйти из области видимости оно не может. Компилятор называет и причину, и оба выхода:

An `Allocation` owns heap storage and must be consumed before it goes out of scope.
Deallocate it with `dealloc(allocation^)`, or call `unsafe_leak()` to take ownership
of the underlying pointer.
Как исправить

dealloc(block^) — освободить. Либо unsafe_leak(), если ответственность за память вы забираете себе.

Это заметно лучше, чем в C: утечка там — молчаливая ошибка времени выполнения, здесь — ошибка сборки.

owners.mojo
# Три вида указателей и одна задача, которую в Mojo 1.0 приходится
# решать обходным путём.
from std.memory import Pointer, OwnedPointer, ArcPointer, Layout, dealloc
@fieldwise_init
struct Config(Movable, Deinitable):
"""Настройки, которые читают из нескольких мест."""
var retries: Int
@fieldwise_init
struct Buffer(Movable, Deinitable):
"""Буфер, у которого владелец ровно один."""
var size: Int
# Односвязный список в Mojo 1.0 через указатели не построить:
# структура не может ссылаться на себя. Ссылки заменяем индексами.
@fieldwise_init
struct Item(Copyable, Movable):
var value: Int
var next: Int # -1 — конец списка
def main():
print("1. Pointer — ссылка на уже существующее значение")
var counter = 10
var view = Pointer(to=counter)
counter += 5
print(" значение через указатель:", view[])
print("2. OwnedPointer — владелец ровно один")
var buffer = OwnedPointer(Buffer(4096))
print(" размер буфера:", buffer[].size)
# var copy = buffer — не соберётся: копировать владельца нельзя
print("3. ArcPointer — владельцев несколько, есть счётчик")
var config = ArcPointer(Config(3))
print(" ссылок:", config.count())
var shared = config
print(" после копии:", config.count())
print(" попыток из обеих копий:", config[].retries, shared[].retries)
print("4. Сырая память: выделили — обязаны освободить")
var block = alloc(Layout[Int](count=3))
var raw = block.unsafe_ptr()
for i in range(3):
raw.unsafe_store(i, i * 100)
print(" содержимое:", raw.unsafe_load(0), raw.unsafe_load(2))
dealloc(block^)
print("5. Список без указателей: ссылки — индексы массива")
var items = List[Item]()
items.append(Item(1, 1))
items.append(Item(2, 2))
items.append(Item(3, -1))
print(" обход:", end="")
var i = 0
while i >= 0:
print(" ", items[i].value, end="")
i = items[i].next
print()
Результат
1. Pointer — ссылка на уже существующее значение
   значение через указатель: 15
2. OwnedPointer — владелец ровно один
   размер буфера: 4096
3. ArcPointer — владельцев несколько, есть счётчик
   ссылок: 1
   после копии: 2
   попыток из обеих копий: 3 3
4. Сырая память: выделили — обязаны освободить
   содержимое: 0 200
5. Список без указателей: ссылки — индексы массива
   обход:  1  2  3

🎯 Проверь себя

Чем `Pointer` отличается от `OwnedPointer`?

Владением. Pointer ничем не владеет и ничего не освобождает — это вид на значение, которое живёт где-то ещё. OwnedPointer кладёт значение в кучу и владеет им единолично, поэтому его нельзя копировать: копия означала бы второго владельца и двойное освобождение.

Почему `struct Node` с полем `next: OwnedPointer[Node]` не собирается, а с `Pointer[Node, MutUntrackedOrigin]` — собирается?

Дело во владении. Чтобы уничтожить Node, нужно уничтожить OwnedPointer[Node], для чего нужно уничтожить Node — круг не размыкается, и компилятор говорит field 'next' has non-'Deinitable' type. Невладеющий Pointer уничтожать нечего, поэтому рекурсия проходит. Цена — снятые проверки времени жизни: в имени MutUntrackedOrigin это сказано прямо.

`ArcPointer[Node]` как поле структуры компилируется. Почему тогда списка на нём не построить?

Потому что список надо чем-то заканчивать, а нулевого указателя в Mojo нет — «ничего» выражают через Optional. И вот Optional[ArcPointer[Node]] уже даёт struct has recursive reference to itself. Без Optional список бесконечен, с ним не компилируется.

Компилятор советует заменить `alloc[Int](4)` на `unsafe_alloc`, но `from std.memory import unsafe_alloc` не работает. В чём дело?

В пути импорта: функция лежит в модуле, а не в пакете — from std.memory.alloc import unsafe_alloc. Работать она будет, но вернёт голый указатель без обёртки Allocation, а значит, и без проверки «не забыли освободить». Для нового кода надёжнее alloc(Layout[Int](count=4)) с dealloc(block^).

Что произойдёт, если забыть `dealloc`?

Программа не соберётся. Компилятор скажет abandoned without being explicitly destroyed и назовёт оба выхода: dealloc(block^) либо unsafe_leak(). В отличие от C, утечка здесь — ошибка сборки, а не молчаливая проблема времени выполнения.

Когда стоит брать `ArcPointer` вместо `OwnedPointer`?

Только когда владельцев действительно несколько и заранее неизвестно, кто закончит последним. Счётчик ссылок не бесплатен: каждое копирование и уничтожение его меняют, а в многопоточном коде это атомарная операция.

Замыкания и лямбды: списки захвата, которые мы уже видели у vectorize, — и почему без них компилятор отказывается угадывать.

Примеры проверены на Mojo 1.1.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0