Panzerschrek[CN]
> Не запрещать, но при попытке вызова проверять, что он происходит в unsafe
> блоке?
Вродь самое логичное
1 frag / 2 deaths
> > Не запрещать, но при попытке вызова проверять, что он происходит в unsafe
> > блоке?
> Вродь самое логичное
А что делать в этом случае?
struct S { fn constructor() unsafe {} } struct T { S s; }
Не генерировать конструктор по умолчанию для S? Генерировать его, но сделать unsafe? Выдать ошибку?
Panzerschrek[CN]
> А что делать в этом случае?
Я предлагаю в этом случае заставить пользователя явно написать, что конструктор - ансейф:
struct S { fn constructor() unsafe {} } struct T { S s; fn constructor() unsafe = default; }
И аналогично для остальных генерируемых методов - методы генерируются с помощью специальных объявлений, в этом объявлении сигнатура поддаётся настройке в определённых пределах, в некоторых случаях, если нет ни явного "генерируй", ни явного "не генерируй", компилятор автоматически вставляет подразумеваемый "генерируй" с дефолтной сигнатурой. По дефолту, сигнатура - сейф. Тело метода всегда генерируется одинаково. И иногда так получается, что вызов метода внутри сгенерированного кода - это ансейф без ансейф-контекста, и потому - ошибка компиляции.
1. Либо напиши объявление "генерируй" явно, с явной сигнатурой, и допиши в неё "ансейф".
2. Либо напиши явное объявление "не генерируй".
3. Либо напиши полностью ручную реализацию метода и заверни ансейф как того позволяет конкретный частный случай, если в потомке метод становится сейф.
Имбирная Ведьмочка
> Я предлагаю в этом случае заставить пользователя явно написать, что конструктор - ансейф:
Звучит разумно. Можно даже так запилить.
Кстати, у меня ещё есть кое-какие проблемы в текущей реализации unsafe.
Во-первых, unsafe это блок. А значит не очень удобно делать инициализацию каких-то переменных через unsafe вызов. Приходится писать сначала объявление изменяемой переменной, а после неё unsafe блок с её инициализацией.
Во-вторых, unsafe не доступен в списке инициализации конструктора, что (иногда) бывает полезно. Эта проблема вытекает из предыдущей, ибо блоки доступны только в теле функции, но не в списке инициализации.
Delfigamer предложил бы сделать как в Rust - чтобы блоки возвращали значения. Но я так не хочу, ибо это слишком масштабное изменение. Подумываю, может сделать что-то вроде unsafe выражения, которое возвращает результат. При этом можно заиспользовать то же ключевое слово. Парсинг бы тогда был следующий:
unsafe { // открывается unsafe блок unsafe( // открывается unsafe выражение
Panzerschrek[CN]
> Во-вторых, unsafe не доступен в списке инициализации конструктора
Что это? Звучит как-то крестовато
1 frag / 2 deaths
> Что это? Звучит как-то крестовато
Оно самое.
C++:
struct S { int x; int y; S() : x(0), y(0) {} };
Ü:
struct S { i32 x; i32 y; fn constructor() ( x(0), y(0) ) {} }
Panzerschrek[CN]
Мне кажется это лишний синтаксический конструкт.
В русте проще - можешь для любой структуры универсально поля поимённо перечислить. Но если они приватные - то это можно только в пределах модуля
1 frag / 2 deaths
В Rust нету конструкторов, там только фабричные методы. И местами они выглядят бойлерплейтно.
> Но если они приватные - то это можно только в пределах модуля
В Ü нету модульности. Есть разве что видимость членов класса. А для классов, кстати, инициализаторы {} запрещены, так что там только конструкторы могут быть.
1 frag / 2 deaths
> В русте проще - можешь для любой структуры универсально поля поимённо
> перечислить. Но если они приватные - то это можно только в пределах модуля
Я рассказывал в соседней теме, у руста и крестов есть фундаментальное различие в том, как вообще работают объекты и переменные.
В С++, переменная - это указатель, а объект - это участок памяти по этому указателю. Инициализировать объект - это значит записать в память правильные байты. А в промежуток времени между аллокацией и инициализацией - в памяти лежит убэ, если случайно к нему прикоснёшься - превратишься в тыкву.
В расте, не вдаваясь в нюансы - памяти не существует.
Поскольку в расте памяти не существует, то объекты не хранятся в каком-то месте, а существуют прямо сами по себе.
Поскольку в расте памяти не существует, то в нём нет раздельных шагов "аллокации" и "инициализации", вместо этого - "рождение" объекта, которое создаёт полностью готовый объект из ничего за один неделимый шаг.
А ещё в расте не может быть "неинициализированной памяти". Потому что в нём нет вообще никакой памяти.
Соответственно, в расте, инициализация как в крестах в принципе не имеет смысла - пока в переменную не присвоили готовый объект, в этой переменной вообще ничего нет и инициализировать там нечего. А когда объект уже присвоили - то это обязательно будет уже готовый объект, и инициализировать там опять будет нечего.
Нюанс - в терминологии раста всё-таки есть понятие "память", но она не имеет совершенно никакого отношения к той памяти, с которой работают кресты.
Имбирная Ведьмочка
> В расте, не вдаваясь в нюансы - памяти не существует
Слишком грубо упростил.
Там, насколько я понимаю, нет привязки объектов к памяти, которая есть в крестах. Это позволяет компилятору перемещать без всяких извращений объекты туда-сюда из одного места в другое.
В Ü почти так же.
> А ещё в расте не может быть "неинициализированной памяти"
std::mem::uninitialized - а это что тогда такое?
Panzerschrek[CN]
> std::mem::uninitialized - а это что тогда такое?
Это нюанс. :3
А с точки зрения фронт-энда, std::mem::MaybeUninit - это просто ещё один тип объектов. И объекты этого типа ведут себя точно так же, как и все остальные - рожаются из функций, борровятся на ссылки, могут рожать другие объекты через функции и дропаются по окончанию. И просто имеют слово "mem" в названии.
Это в чём-то аналогично векторным интринсикам в крестах - они позволяют намекнуть, что было бы неплохо выполнить вот эту конкретную инструкцию процессора; но в конечном итоге - никакая это не инструкция, а обычная крестовая функция с обычной сигнатурой, которую точно так же можно заоверлоадить, передать по указателю, за-ADLукапить, вот это вот всё.
Вот и в расте можно родить "объект неинициализированной памяти", передать его по ссылке в иностранный код, и затем передать в функцию "перероди его в объект внутри". Но это уже - частный сценарий использования объектной модели, а не составная её часть.
Если не вдаваться в нюансы и под словом "память" подразумевать именно то, что обычно подразумевается простыми смертными - эн миллиардов пронумерованных коробочек с байтами, которые пишутся и читаются, существуют независимо от действий программы и доступны в любое время из любого места, был бы правильный указатель - то вот такой памяти в расте не существует.
Имбирная Ведьмочка
> вот такой памяти в расте не существует
Софистика.
Она существует на низком уровне. И можно не сильно сложным способом напороться на исчерпания адресного пространства.
И даже где-то в недрах небезопасной документации есть гарантии на расположение объектов в памяти.
Panzerschrek[CN]
> Она существует на низком уровне.
Если рассматривать язык как набор правил "если написано вот это, то должно произойти вот это"; то в крестах как в языке - существует, в расте как в языке - нет, не существует.
Panzerschrek[CN]
> Софистика.
Разница в подходах - реальная. Можно описать её другими словами. Например - "кресты насквозь императивные, а раст - на самом деле две функциональщины в длинном пальто и шляпе". А можно - "в расте объекты не лежат в памяти, а существуют сами по себе, без адреса, без байтов и, иногда, даже без фиксированного размера". Второе, мне кажется, вполне неплохо передаёт суть.
А "памяти не существует" - это всего лишь укороченная версия для тех, кто только пришёл с крестов и ещё не понимает всех тонкостей, предназначенная для того, чтобы дать общую идею происходящего.
Вообще, когда человек изучает новый материал (физику, лингвистику, раст), у него практически всегда есть априорные представления о том, как, по его мнению, всё должно быть. И в процессе изучения - он не столько приобретает новые знания, сколько корректирует уже имеющиеся представления. Вот читает абзац, типа, "let mut позволяет делать мутабельные переменные", и сравнивает его со своими представлениями, типа, "переменная - это область памяти". Если никаких несоответствий не обнаруживается - человек совершенно ничему не научается, и идёт дальше с мыслями "какой же я умный, всё так просто и понятно". А потом берёт и пишет какую-нибудь фигню, и даже не подозревает, какую же фигню он пишет. И недоумевает, что же компилятор на него ругается, всё же нормально написано, вон в крестах то же самое писал - и там всё работало, а тут вдруг берёт и не работает, как же так.
"В расте памяти не существует" - это фраза, у которой главная цель - это именно побороть неправильные начальные представления. Чтобы человек не намазывал костыли на изначально неправильную ментальную модель, а сразу приступил к сборке новой, пока это не так сложно (чем дольше человек не видит противоречий со своими текущими представлениями, тем сложнее ему впоследствии от них избавиться).
Чтобы, когда человек попытается подумать "переменная - это область памяти", он тут же вспомнил - "памяти не существует", и пришёл к более правильному представлению "переменная - это имя для объекта".
Чтобы, когда человек подумает об "адресе объекта", он тут же вспомнит - "памяти не существует", задастся вопросом - "если памяти не существует, то куда тогда указывает адрес?", и придёт к просветлению - у объектов нет адреса. Поэтому и нужен std::pin.
А когда он подумает о том, чтобы "инициализировать переменную в конструкторе", он заодно сразу же и подумает "погоди, но если памяти нет... чего мы тогда будет инициализировать?"
И сразу же станет просто и понятно. Каким образом перемещаются объекты. Зачем нужен Пин. Почему let x: T; не вызывает дефолт-конструктор. И куда пропали списки инициализации.
Имбирная Ведьмочка
> памяти не существует
До первого unsafe вызова внешней функции. Когда надо будет вызвать, соазу полезешь в документацию, чтобы узнать, в каких случаях адрес меняться не будет.
> А когда он подумает о том, чтобы "инициализировать переменную в конструкторе",
> он заодно сразу же и подумает "погоди, но если памяти нет... чего мы тогда
> будет инициализировать?"
Ну допустим, памяти нету. Тогда конструктор это функция описания шагов конструирования объекта.
> Почему let x: T; не вызывает дефолт-конструктор.
Потому, что Rust - бойлерплейтное говно, в котором надо руками звать в таком случае фабричую функцию (default) и заниматься прочим онанизмом с взятием/разыменованием указателей, заворачиванием значений в optional и т. д.