Skip to content

Сравнение с Rust и C++

This content is not available in your language yet.

Если вы писали на Rust, половина этого раздела показалась вам знакомой. Если на C++ — знакомой показалась другая половина. Эта глава расставляет точки: что совпадает, что нет и почему.

Единственный владелец. У значения один хозяин, передача владения — явная операция.

Использовать после передачи нельзя. Обе системы ловят это на этапе компиляции, просто говорят по-разному:

ЯзыкСообщение
Rusterror[E0382]: borrow of moved value: `a`
Mojoerror: use of uninitialized value 'a'

Ссылка не переживёт источник. Rust называет это временем жизни ('a), Mojo — происхождением (origin). Идея одна: компилятор знает, откуда взялась ссылка, и не даёт ей пережить владельца.

Инвалидация ссылок ловится. Классический пример — взять ссылку на элемент и добавить элемент в контейнер:

ЯзыкЧто будет
Rusterror[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
Mojoerror: use of invalidated interior reference 'xs["element"]'
C++компилируется, печатает мусор

Последняя строка — не преувеличение. Мы взяли указатель на первый элемент std::vector, добавили сто элементов и напечатали значение по указателю: получили 1458768981 — обрывок чужого адреса из освобождённой памяти. При каждом запуске число другое, и компилятор ни разу не сказал ни слова.

Главное правило Rust — одна изменяемая ссылка или сколько угодно неизменяемых. Правило спасает от гонок данных, но за него платят: даже безобидный код приходится переписывать.

let mut v = vec![1, 2, 3];
let first = &mut v[0];
let second = &mut v[1]; // error[E0499]
*first = 10;
*second = 20;

Rust запрещает это, хотя элементы разные и конфликта нет: cannot borrow `v` as mutable more than once at a time. Компилятор сам подсказывает обходной путь — split_at_mut.

Обратите внимание на две последние строки: без них пример собрался бы. Rust давно не считает заимствование живым до конца блока — оно заканчивается на последнем использовании. Ошибка появляется именно тогда, когда обеими ссылками действительно пользуются.

В Mojo то же самое просто работает:

def main():
var scores: List[Int] = [1, 2, 3]
var low = Pointer(to=scores[0])
var high = Pointer(to=scores[1])
low[] = 10
high[] = 20
print(scores[0], scores[1], scores[2])
Результат
10 20 3

Но исключительность всё-таки есть — на границе вызова

Заголовок раздела «Но исключительность всё-таки есть — на границе вызова»

Это не значит, что Mojo разрешает всё. Проверка есть, только она уже и точнее: два изменяемых аргумента одного вызова не могут указывать на одно значение.

error: aliasing values passed mutably to 'x' argument and passed mutably to 'y' argument in 'bump' call
Что это значит

Вы передали одну и ту же переменную в два mut-аргумента: bump(value, value). Внутри функции получились бы две изменяемые ссылки на одно место — и результат зависел бы от порядка операций.

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

Передайте разные значения либо перестройте функцию так, чтобы изменяемый аргумент был один.

Разница в подходе: Rust требует исключительности от каждого заимствования, Mojo проверяет её только на границе вызова. Проверок меньше — и ошибок ловится меньше тоже.

Rust требует объявлять параметры времени жизни и продевать их через сигнатуры. В Mojo вы пишете место, откуда берётся ссылка:

ЗадачаRustMojo
ссылка на аргументfn f<'a>(x: &'a str) -> &'a strdef f(ref x: String) -> ref [x] String
ссылка на полеfn get(&self) -> &i32def get(ref self) -> ref [self.value] Int
ссылка на элементfn first(&self) -> &i32def first(ref self) -> ref [self.items[0]] Int

Отдельных имён вроде 'a придумывать не нужно: их роль играет само выражение доступа.

Структуру, которая хранит ссылку, параметризуют происхождением — это прямой аналог struct Holder<'a>:

@fieldwise_init
struct View[o: Origin]:
var target: Pointer[Int, Self.o]
def show(self):
print(self.target[])
def main():
var value = 42
var view = View(Pointer(to=value))
view.show()
Результат
42

Обратите внимание на Self.o: внутри тела структуры к её собственным параметрам обращаются через Self — то же правило, что и в главе Структуры.

Здесь Mojo расходится и с Rust, и с C++. В обоих значение живёт до конца области видимости; в Mojo — до последнего использования.

Один и тот же сценарий на трёх языках:

Rust C++ Mojo
────────────────────────────────────────────
используем используем используем
конец main конец main уничтожен ←
уничтожен ← уничтожен ← конец main

Мы собрали и запустили все три программы: Rust и C++ печатают «уничтожен» последней строкой, Mojo — сразу после последнего обращения к значению. Для больших буферов и открытых ресурсов это заметная разница.

Перемещённое значение не остаётся «пустым объектом»

Заголовок раздела «Перемещённое значение не остаётся «пустым объектом»»

В C++ после std::move объект жив: он в «валидном, но неуточнённом состоянии» (valid but unspecified). Не путать с неопределённым поведением: обращаться к объекту законно, стандарт лишь не обещает, что именно вы получите. На практике это и есть источник сюрпризов.

std::string a = "файл";
std::string b = std::move(a);
std::cout << "a = '" << a << "', длина " << a.size();

В нашем прогоне вывелось a = '', длина 0: строка опустела, но код собрался и выполнился. В Mojo такой код просто не компилируется — переменная после ^ не существует, а не «пуста».

C++ копирует по умолчанию. Передали вектор по значению — получили копию, и никто вас не предупредил:

void takes(std::vector<int> v) { ... }
std::vector<int> big(1000000, 7);
takes(big); // молча скопировался миллион элементов

Проверено: копия действительно создаётся — адрес буфера внутри функции другой, — оригинал остаётся цел, предупреждений ноль.

В Mojo молчаливой копии не бывает вовсе, но по другой причине, чем можно подумать. Прямой перевод этой сигнатуры — def takes(v: List[Int]) — компилируется и работает, только копии там нет: аргумент по умолчанию заимствуется. А если функции действительно нужно владение (def takes(var v: List[Int])), компилятор откажется собирать вызов и попросит выбрать между .copy() и ^.

Rust ведёт себя как Mojo: Vec не Copy, передача по значению перемещает, копия — явный clone().

ВозможностьЕсть вКомментарий
Разделяемая изменяемость с проверкой в рантайме (RefCell, Cell)RustВ Mojo таких типов нет
Слабые ссылки (Weak, weak_ptr)Rust, C++Отдельного типа нет, хотя счётчик слабых ссылок у ArcPointer есть
Проверка гонок данных на этапе компиляцииRust (Send, Sync)В Mojo такой системы трейтов нет
Наследование и виртуальные функцииC++В Mojo нет ни классов, ни наследования структур
Исключения с иерархией типовC++У функции в Mojo один тип ошибки: Error или своя структура (raises MyError); наследования нет

Первые три строки — следствие возраста: самому языку три года, а стабильному API — год. Две последние — осознанные решения, о которых мы говорили в главах Структуры и Обработка ошибок.

borrowing.mojo
# Что Mojo разрешает и запрещает — там, где Rust и C++ ведут себя иначе.
@fieldwise_init
struct Noisy:
"""Сообщает о своём уничтожении, чтобы был виден момент."""
var name: String
def __deinit__(deinit self):
print(" уничтожен:", self.name)
@fieldwise_init
struct View[o: Origin]:
"""Структура, хранящая ссылку. Прямой аналог Rust-структуры с 'a."""
var target: Pointer[Int, Self.o]
def show(self):
"""Читает значение по ссылке."""
print(" через ссылку:", self.target[])
def main():
print("1. значение умирает сразу после последнего использования")
var first = Noisy("первый")
print(" используем:", first.name)
print(" а эта строка печатается уже после уничтожения")
print("2. две изменяемые ссылки на разные элементы — можно")
var scores: List[Int] = [1, 2, 3]
var low = Pointer(to=scores[0])
var high = Pointer(to=scores[1])
low[] = 10
high[] = 20
print(" ", scores[0], scores[1], scores[2])
print("3. структура, которая хранит ссылку")
var value = 42
var view = View(Pointer(to=value))
view.show()
Результат
1. значение умирает сразу после последнего использования
   используем: первый
   уничтожен: первый
   а эта строка печатается уже после уничтожения
2. две изменяемые ссылки на разные элементы — можно
   10 20 3
3. структура, которая хранит ссылку
   через ссылку: 42

Пришли из Rust. Модель узнаваема, но заимствования считать не нужно: исключительность проверяется только между изменяемыми аргументами одного вызова. Времена жизни пишутся выражением доступа вместо 'a. Уничтожение происходит раньше, чем вы привыкли.

Пришли из C++. ^ похож на std::move, но после него переменной не существует, а не «она в неопределённом состоянии». Копии не случаются молча. Деструктор срабатывает не в конце блока, а сразу после последнего использования.

Пришли из Python. Всё это — цена за отсутствие сборщика мусора. Взамен вы получаете предсказуемое освобождение ресурсов и скорость, ради которой Mojo и создавался.

Раздел о владении закончен. Дальше — метапрограммирование: код, который выполняется при сборке программы.

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

Почему в Mojo можно взять две изменяемые ссылки на разные элементы списка, а в Rust нельзя?

Rust проверяет исключительность заимствований везде и не различает элементы одного контейнера — отсюда E0499. Mojo проверяет уже: только то, что два изменяемых аргумента одного вызова не указывают на одно значение. Меньше борьбы с компилятором, но и гарантий чуть меньше.

Чем `^` в Mojo отличается от `std::move` в C++?

После std::move объект жив и находится в валидном, но неопределённом состоянии — им можно пользоваться и получить сюрприз. После ^ переменной просто нет: обращение к ней не компилируется.

Когда уничтожается значение в Rust, C++ и Mojo?

В Rust и C++ — в конце области видимости. В Mojo — сразу после последнего использования, то есть обычно раньше. Проверено запуском одинакового сценария на всех трёх языках.

Чем в Mojo заменяют параметр времени жизни `'a` из Rust?

Выражением доступа: ref [x], ref [self.value], ref [items[0]]. Отдельное имя придумывать не нужно. Структуру, хранящую ссылку, параметризуют происхождением — struct View[o: Origin], это прямой аналог struct View<'a>.

«Владение значениями» закончено. Теперь понятно, почему var b = a требует уточнения, что означают mut, ref и out, когда именно уничтожается значение и как компилятор доказывает, что ссылка безопасна.

Дальше — Метапрограммирование: параметры в квадратных скобках, comptime, трейты и то, как компилятор генерирует специализированный код под каждый набор параметров.

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

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