Как Rust обеспечивает безопасность указателя только во время компиляции?
Я где-то читал, что на языке, в котором есть указатели, компилятор не может полностью решить во время компиляции, правильно ли все указатели используются и / или действительны (ссылаются на живой объект) по разным причинам, поскольку это по сути, составляют решение проблемы остановки. Это не удивительно, интуитивно, потому что в этом случае мы могли бы вывести поведение программы во время компиляции во время компиляции, аналогично тому, что указано в этом связанном вопросе.
Однако из того, что я могу сказать, язык Rust требует, чтобы проверка указателя выполнялась полностью во время компиляции (нет неопределенного поведения, связанного с указателями, по крайней мере, "безопасными" указателями, и нет времени выполнения "недопустимый указатель" или "нулевой указатель") исключение тоже).
Предполагая, что компилятор Rust не решает проблему остановки, где ложь?
- Действительно ли проверка указателей не выполняется полностью во время компиляции, и умные указатели Rust все еще вводят некоторые накладные расходы времени выполнения по сравнению, скажем, с необработанными указателями в C?
- Или же возможно, что компилятор Rust не может принимать полностью правильные решения, и ему иногда нужно просто довериться программисту ™, возможно, используя одну из аннотаций на весь срок службы (те, что с
<'lifetime_ident>
синтаксис)? В этом случае это означает, что гарантия безопасности указателя / памяти не 100%, и все еще полагается на программиста, пишущего правильный код? - Другая возможность состоит в том, что указатели Rust не являются "универсальными" или в некотором смысле ограничены, так что компилятор может выводить свои свойства полностью во время компиляции, но они не так полезны, как, например, необработанные указатели в C или интеллектуальные указатели в C++.
- Или, может быть, это что-то совершенно другое, и я неправильно понимаю один или несколько
{ "pointer", "safety", "guaranteed", "compile-time" }
,
3 ответа
Отказ от ответственности: я немного спешу, так что это немного извилистый. Не стесняйтесь убирать это.
Одна хитрая уловка, которую ненавидят дизайнеры языка ™, в основном такова: ржавчина может только рассуждать о 'static
время жизни (используется для глобальных переменных и других вещей времени жизни всей программы) и время жизни стековых (то есть локальных) переменных: оно не может выражать или рассуждать о времени жизни распределений кучи.
Это означает несколько вещей. Прежде всего, все типы библиотек, которые имеют дело с распределением кучи (т.е. Box<T>
, Rc<T>
, Arc<T>
) все владеют тем, на что они указывают. В результате им на самом деле не нужны жизни, чтобы существовать.
Вам нужно время жизни, когда вы получаете доступ к содержимому умного указателя. Например:
let mut x: Box<i32> = box 0;
*x = 42;
То, что происходит за кулисами во второй строке, таково:
{
let box_ref: &mut Box<i32> = &mut x;
let heap_ref: &mut i32 = box_ref.deref_mut();
*heap_ref = 42;
}
Другими словами, потому что Box
не волшебство, мы должны сказать компилятору, как превратить его в обычный запуск указателя заимствованной функции. Это то, что Deref
а также DerefMut
черты для. Возникает вопрос: что именно heap_ref
?
Ответ на это в определении DerefMut
(по памяти, потому что я спешу)
trait DerefMut {
type Target;
fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}
Как я уже говорил, Руст абсолютно не может говорить о "кучах жизней". Вместо этого он должен связать время жизни выделенной кучи i32
до единственного другого времени жизни, которое оно имеет под рукой: время жизни Box
,
Это означает, что "сложные" вещи не имеют выраженного времени жизни и, следовательно, должны владеть тем, чем управляют. Когда вы конвертируете сложный умный указатель / дескриптор в простой заимствованный указатель, это тот момент, когда вам нужно ввести время жизни, а вы обычно просто используете время жизни самого дескриптора.
На самом деле, я должен уточнить: под "временем жизни дескриптора" я на самом деле имею в виду "время жизни переменной, в которой в данный момент хранится дескриптор": время жизни действительно для хранения, а не для значений. Это типично, почему новички в Rust запутываются, когда они не могут понять, почему они не могут сделать что-то вроде:
fn thingy<'a>() -> (Box<i32>, &'a i32) {
let x = box 1701;
(x, &x)
}
"Но... я знаю, что коробка будет продолжать жить, почему компилятор говорит, что это не так?!" Потому что Руст не может рассуждать о времени жизни кучи и должен прибегнуть к связыванию времени жизни &x
к переменной x
, а не выделение кучи, на которое оно указывает.
Действительно ли проверка указателей не выполняется полностью во время компиляции, и умные указатели Rust все еще вводят некоторые накладные расходы времени выполнения по сравнению, скажем, с необработанными указателями в C?
Существуют специальные проверки во время выполнения для вещей, которые не могут быть проверены во время компиляции. Они обычно находятся в cell
обрешетка. Но в целом Rust проверяет все во время компиляции и должен генерировать тот же код, что и в C (если ваш C-код не выполняет неопределенных задач).
Или возможно, что компилятор Rust не может принимать полностью правильные решения, и ему иногда нужно просто довериться программисту ™, возможно, используя одну из аннотаций времени жизни (тех, которые имеют синтаксис <'life_ident>)? В этом случае это означает, что гарантия безопасности указателя / памяти не 100%, и все еще полагается на программиста, пишущего правильный код?
Если компилятор не может принять правильное решение, вы получаете ошибку времени компиляции, сообщающую, что компилятор не может проверить, что вы делаете. Это также может ограничить вас от того, что вы знаете правильно, но компилятор этого не делает. Вы всегда можете пойти в unsafe
код в этом случае. Но, как вы правильно поняли, компилятор частично полагается на программиста.
Компилятор проверяет реализацию функции, чтобы увидеть, делает ли она именно то, что говорят времена жизни. Затем на месте вызова функции он проверяет, правильно ли программист использует функцию. Это похоже на проверку типов. Компилятор C++ проверяет, возвращаете ли вы объект правильного типа. Затем он проверяет на сайте вызова, хранится ли возвращенный объект в переменной правильного типа. Программист функции никогда не может нарушить обещание (кроме случаев, когда unsafe
используется, но вы всегда можете позволить компилятору установить, что нет unsafe
используется в вашем проекте)
Ржавчина постоянно улучшается. Больше вещей может стать легальным в Rust, когда компилятор станет умнее.
Другая возможность состоит в том, что указатели Rust не являются "универсальными" или в некотором смысле ограничены, так что компилятор может выводить свои свойства полностью во время компиляции, но они не так полезны, как, например, необработанные указатели в C или интеллектуальные указатели в C++.
Есть несколько вещей, которые могут пойти не так в C:
- свисающие указатели
- двойной бесплатный
- нулевые указатели
- дикие указатели
Такого не бывает в безопасной Русте.
- Вы никогда не можете иметь указатель, который указывает на объект, который больше не находится в стеке или куче. Это доказано во время компиляции на протяжении всей жизни.
- У вас нет ручного управления памятью в Rust. Использовать
Box
выделить ваши объекты (похожие, но не равныеunique_ptr
в C++) - Опять же, нет ручного управления памятью.
Box
ES автоматически освобождает память. - В Safe Rust вы можете создать указатель на любое место, но вы не можете разыменовать его. Любая ссылка, которую вы создаете, всегда связана с объектом.
Есть несколько вещей, которые могут пойти не так в C++:
- все, что может пойти не так в C
- SmartPointers только поможет вам не забыть звонить
free
, Вы все еще можете создавать висячие ссылки:auto x = make_unique<int>(42);
auto& y = *x;
x.reset();
y = 99;
Руст исправляет те:
- см выше
- пока
y
существует, вы не можете изменятьx
, Это проверяется во время компиляции и не может быть обойдено большим количеством косвенных указаний или структур.
Я где-то читал, что на языке, в котором есть указатели, компилятор не может полностью решить во время компиляции, правильно ли все указатели используются и / или действительны (ссылаются на живой объект) по разным причинам, поскольку это по сути, составляют решение проблемы остановки.
Rust не доказывает, что все указатели используются правильно. Вы все еще можете писать фиктивные программы. Rust доказывает, что вы не используете недействительные указатели. Rust доказывает, что у вас никогда нет нулевых указателей. Rust доказывает, что у вас никогда нет двух указателей на один и тот же объект, за исключением случаев, когда все эти указатели не являются изменяемыми (const). Rust не позволяет вам писать какие-либо программы (поскольку в них входят программы, нарушающие безопасность памяти). Прямо сейчас Rust все еще не позволяет вам писать некоторые полезные программы, но есть планы разрешить писать больше (легальных) программ на безопасном Rust.
Это не удивительно, интуитивно, потому что в этом случае мы могли бы вывести поведение программы во время компиляции во время компиляции, аналогично тому, что указано в этом связанном вопросе.
Пересмотрим пример в указанном вами вопросе о проблеме остановки:
void foo() {
if (bar() == 0) this->a = 1;
}
Приведенный выше код C++ будет выглядеть в Rust одним из двух способов:
fn foo(&mut self) {
if self.bar() == 0 {
self.a = 1;
}
}
fn foo(&mut self) {
if bar() == 0 {
self.a = 1;
}
}
Для произвольного bar
Вы не можете доказать это, потому что это может получить доступ к глобальному состоянию. Руст скоро становится const
функции, которые могут быть использованы для вычисления вещи во время компиляции (аналогично constexpr
). Если bar
является const
, это становится тривиальным, чтобы доказать, если self.a
установлен в 1
во время компиляции. Кроме этого, без pure
функции или другие ограничения содержимого функции, вы никогда не сможете доказать, self.a
установлен в 1
или нет.
Rust в настоящее время не волнует, называется ли ваш код или нет. Заботится ли память о self.a
все еще существует во время назначения. self.bar()
никогда не сможет уничтожить self
(кроме как в unsafe
код). Therefor self.a
всегда будет доступен внутри if
ветка.
Большая часть ссылок на Rust гарантируется строгими правилами:
- Если у вас есть постоянная ссылка (
&
), вы можете клонировать эту ссылку и передать ее, но не создавать изменяемую&mut
ссылка из этого. - Если изменчивый (
&mut
) ссылка на объект существует, никакая другая ссылка на этот объект не может существовать. - Ссылка не может пережить объект, на который она ссылается, и все функции, управляющие ссылками, должны объявлять, как ссылки из их ввода и вывода связаны, используя аннотации времени жизни (например,
'a
).
Таким образом, с точки зрения выразительности, мы фактически более ограничены, чем при использовании простых необработанных указателей (например, построение структуры графа невозможно, используя только безопасные ссылки), но эти правила могут быть эффективно проверены во время компиляции.
Тем не менее, все еще возможно использовать необработанные указатели, но вы должны заключить код, работающий с ними, в unsafe { /* ... */ }
блок, говорящий компилятору "Поверь мне, я знаю, что я здесь делаю". Это то, что некоторые специальные умные указатели делают внутри, такие как RefCell
, что позволяет вам проверять эти правила во время выполнения, а не во время компиляции, чтобы получить выразительность.