Сравнение Rust и С++ на примерах
Предисловие Вот и обещанное сравнение языков. Примеры, конечно, искуственные, так что используйте своё воображение, чтобы оценить масштабы угрозы в реальном мире.Все C++ программы были собраны при помощи gcc-4.7.2 в режиме c++11, используя online compiler. Программы на Rust были собраны последней версией Rust (nightly, 0.11-pre), используя rust playpen.
Я знаю, что C++14 (и далее) будет залатывать слабые места языка, а также добавлять новые возможности. Размышления на тему того, как обратная совместимость мешает C++ достичь звёзд (и мешает ли), выходят за рамки данной статьи, однако мне будет интересно почитать Ваше экспертное мнение в комментариях. Также приветствуется любая информация о D.
Проверка типов шаблона
Автор С++ уже давно недоволен тем, как шаблоны реализованы в языке, назвав их «compile-time duck typing» в недавнем выступлении на Lang-NEXT. Проблема заключается в том, что не всегда понятно, чем инстанцировать шаблон, глядя на его объявление. Ситуация ухудшается монстрообразными сообщениями об ошибках. Попробуйте собрать, к примеру, вот такую программу:
#include
trait Sortable {}
fn sort
Обращение к удалённой памяти
Существует целый класс проблем с С++, выражающихся в неопределённом поведении и падениях, которые возникают из-за попытки использовать уже удалённую память.Пример:
int main () {
int *x = new int (1);
delete x;
*x = 0;
}
В Rust такого рода проблемы невозможны, так как не существует команд удаления памяти. Память на стеке живёт, пока она в области видимости, и Rust не допускает, чтобы ссылки на неё пережили эту область (смотрите пример про потерявшийся указатель). Если же память выделена в куче — то указатель на неё (Box
int *bar (int *p) { return p; } int* foo (int n) { return bar (&n); } int main () { int *p1 = foo (1); int *p2 = foo (2); printf (»%d, %d\n», *p1, *p2); } На выходе: 2, 2
Версия Rust: fn bar<'a>(p: &'a int) → &'a int { return p; } fn foo (n: int) → &int { bar (&n) } fn main () { let p1 = foo (1); let p2 = foo (2); println!(»{}, {}», *p1, *p2); } Ругательства компилятора: demo:5:10: 5:11 error: `n` does not live long enoughdemo:5 bar (&n)^demo:4:24: 6:2 note: reference must be valid for the anonymous lifetime #1 defined on the block at 4:23…demo:4 fn foo (n: int) → &int {demo:5 bar (&n)demo:6 }demo:4:24: 6:2 note: …but borrowed value is only valid for the block at 4:23demo:4 fn foo (n: int) → &int {demo:5 bar (&n)demo:6 }
Неинициированные переменные
#include
Более идиоматичный (и работающий) вариант этой функции выглядел бы так: fn minval (A: &[int]) → int { A.iter ().fold (A[0], |u,&a| { if a
int main () { A a (1), b=a; } Собирается, однако падает при выполнении: *** glibc detected *** demo: double free or corruption (fasttop): 0×0000000000601010 ***
То же самое на Rust:
struct A{
x: Box
void swap_from (X& x, const X& y) { x.a = y.b; x.b = y.a; } int main () { X x = {1,2}; swap_from (x, x); printf (»%d,%d\n», x.a, x.b); } Выдаёт нам: 2,2
Функция явно не ожидает, что ей передадут ccылки на один и тот же объект. Чтобы убедить компилятор, что ссылки уникальные, в С99 придумали restrict, однако он служит лишь подсказкой оптимизатору и не гарантирует Вам отсутствия перекрытий: программа будет собираться и исполняться как и раньше.Попробуем сделать то же самое на Rust:
struct X { pub a: int, pub b: int } fn swap_from (x: &mut X, y: &X) { x.a = y.b; x.b = y.a; } fn main () { let mut x = X{a:1, b:2}; swap_from (&mut x, &x); } Выдаёт нам следующее ругательство: demo:7:24: 7:25 error: cannot borrow `x` as immutable because it is also borrowed as mutabledemo:7 swap_from (&mut x, &x);^demo:7:20: 7:21 note: previous borrow of `x` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `x` until the borrow endsdemo:7 swap_from (&mut x, &x);^demo:7:26: 7:26 note: previous borrow ends heredemo:7 swap_from (&mut x, &x);
Как видим, компилятор не позволяет нам ссылаться на одну и ту же переменную через »&mut» и »&» одновременно, тем самым гарантируя, что изменяемую переменную никто другой не сможет прочитать или изменить, пока действительна &mut ссылка. Эти гарантии обсчитываются в процессе сборки и не замедляют выполнение самой программы. Более того, этот код сибирается так, как если бы мы на C99 использовали restrict указатели (Rust предоставляет LLVM информацию об уникальности ссылок), что развязывает руки оптимизатору.Испорченный итератор
#include
Попробуем перевести на Rust:
fn main () {
let mut v: Vec
Опасный Switch
#include
class Resource { int *value; public: Resource (): value (NULL) {} ~Resource () {delete value;} int *acquire () { if (! value) { value = new int (0); } return value; } };
void* function (void *param) { int *value = ((Resource*)param)→acquire (); printf («resource: %p\n», (void*)value); return value; }
int main () { Resource res; for (int i=0; i<5; ++i) { pthread_t pt; pthread_create(&pt, NULL, function, &res); } //sleep(10); printf("done\n"); } Порождает несколько ресурсов вместо одного: doneresource: 0x7f229c0008c0resource: 0x7f22840008c0resource: 0x7f228c0008c0resource: 0x7f22940008c0resource: 0x7f227c0008c0
Это типичная проблема синхронизации потоков, которая возникает при одновременном изменении объекта несколькими потоками. Попробуем написать то же на Rust:
struct Resource {
value: Option
fn main () { let mut res = Resource: new (); for _ in range (0,5) { spawn (proc () { let ptr = res.acquire (); println!(«resource {}», ptr) }) } } Получаем ругательство, ибо нельзя вот так просто взять и мутировать общий для потоков объект. demo:20:23: 20:26 error: cannot borrow immutable captured outer variable in a proc `res` as mutabledemo:20 let ptr = res.acquire ();
Вот так может выглядеть причёсанный код, который удовлетворяет компилятор: extern crate sync; use sync::{Arc, RWLock};
struct Resource {
value: Option
fn main () { let arc_res = Arc: new (RWLock: new (Resource: new ())); for _ in range (0,5) { let child_res = arc_res.clone (); spawn (proc () { let ptr = child_res.write ().acquire (); println!(«resource: {}», ptr) }) } } Он использует примитивы синхронизации Arc (Atomically Reference Counted — для доступа к тому же объекту разными потоками) и RWLock (для блокировки совместного изменения). На выходе получаем: resource: 0×7ff4b0010378resource: 0×7ff4b0010378resource: 0×7ff4b0010378resource: 0×7ff4b0010378resource: 0×7ff4b0010378
Понятное дело, что на С++ тоже можно написать правильно. И на ассемблере можно. Rust просто не даёт Вам выстрелить себе в ногу, оберегая от собственных ошибок. Как правило, если программа собирается, значит она работает. Лучше потерять полчаса на приведение кода в приемлимый для компилятора вид, чем потом месяцами отлаживать ошибки синхронизации (стоимость исправления дефекта).Немного про небезопасный код Rust позволяет Вам играть с голыми указателями сколько угодно, но только внутри блока unsafe{}. Это тот случай, когда Вы говорите компилятору «Не мешай! Я знаю, что делаю.». К примеру, все «чужие» функции (из написанной на С библиотеки, с которой вы сливаетесь) автоматически маркируются как опасные. Философия языка в том, чтобы маленькие куски небезопасного кода были изолированы от основной части (нормального кода) безопасными интерфейсами. Так, например, небезопасные участки можно обнаружить в реализациях классов Cell и Mutex. Изоляция опасного кода позволяет не только значительно сузить область поиска неожиданно возникшей проблемы, но и хорошенько покрыть его тестами (мы дружим с TDD!).Источники Guaranteeing Memory Safety in Rust (by Niko Matsakis)Rust: Safe Systems Programming with the Fun of FP (by Felix Klock II)Lang-NEXT: What — if anything — have we learned from C++? (by Bjarne Stroustrup)Lang-NEXT Panel: Systems Programming in 2014 and Beyond