- PVSM.RU - https://www.pvsm.ru -
Вот и обещанное [1] сравнение языков. Примеры, конечно, искуственные, так что используйте своё воображение, чтобы оценить масштабы угрозы в реальном мире.
Все C++ программы были собраны при помощи gcc-4.7.2 в режиме c++11, используя online compiler [2]. Программы на Rust были собраны последней версией Rust (nightly, 0.11-pre), используя rust playpen [3].
Я знаю, что C++14 (и далее) будет залатывать слабые места языка, а также добавлять новые возможности. Размышления на тему того, как обратная совместимость мешает C++ достичь звёзд (и мешает ли), выходят за рамки данной статьи, однако мне будет интересно почитать Ваше экспертное мнение в комментариях. Также приветствуется любая информация о D.
Автор С++ уже давно недоволен тем, как шаблоны реализованы в языке, назвав их "compile-time duck typing" в недавнем выступлении на Lang-NEXT. Проблема заключается в том, что не всегда понятно, чем инстанцировать шаблон, глядя на его объявление. Ситуация ухудшается монстрообразными сообщениями об ошибках. Попробуйте собрать, к примеру, вот такую программу:
#include <vector>
#include <algorithm>
int main()
{
int a;
std::vector< std::vector <int> > v;
std::vector< std::vector <int> >::const_iterator it = std::find( v.begin(), v.end(), a );
}
Представьте себе радость человека, читающего многостраничное сообщение об ошибке [4], если он создал такую ситуацию случайно.
Шаблоны в Rust проверяются на корректность до их инстанцирования, поэтому есть чёткое разделение между ошибками в самом шаблоне (которых быть не должно, если Вы используете чужой/библиотечный шаблон) и в месте инстанцирования, где всё, что от Вас требуется — это удовлетворить требования к типу, описанные в шаблоне:
trait Sortable {}
fn sort<T: Sortable>(array: &mut [T]) {}
fn main() {
sort(&mut [1,2,3]);
}
Этот код не собирается по очевидной причине:
demo:5:5: 5:9 error: failed to find an implementation of trait Sortable for int
demo:5 sort(&mut [1,2,3]);
Существует целый класс проблем с С++, выражающихся в неопределённом поведении и падениях, которые возникают из-за попытки использовать уже удалённую память.
Пример:
int main() {
int *x = new int(1);
delete x;
*x = 0;
}
В Rust такого рода проблемы невозможны, так как не существует команд удаления памяти. Память на стеке живёт, пока она в области видимости, и Rust не допускает, чтобы ссылки на неё пережили эту область (смотрите пример про потерявшийся указатель). Если же память выделена в куче — то указатель на неё (Box<T>
) ведёт себя точно так же, как и обычная переменная на стеке (удаляется при выходе из зоны видимости). Для совместного использования данных есть подсчёт ссылок (std::rc::Rc<T>
) и сборщик мусора (std::gc::Gc<T>
), оба реализованы как сторонние классы (Вы можете написать свои).
Версия С++:
#include <stdio.h>
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, %dn", *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 enough
demo: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:23
demo:4 fn foo(n: int) -> &int {
demo:5 bar(&n)
demo:6 }
#include <stdio.h>
int minval(int *A, int n) {
int currmin;
for (int i=0; i<n; i++)
if (A[i] < currmin)
currmin = A[i];
return currmin;
}
int main() {
int A[] = {1,2,3};
int min = minval(A,3);
printf("%dn", min);
}
Выдаёт мне 0 на выходе, хотя на самом деле здесь, конечно, неопределённый результат. А вот то же самое на Rust (прямой не-идиоматичный перевод):
fn minval(A: &[int]) -> int {
let mut currmin;
for a in A.iter() {
if *a < currmin {
currmin = *a;
}
}
currmin
}
fn main() {
let A = [1i,2i,3i];
let min = minval(A.as_slice());
println!("{}", min);
}
Не собирается, ошибка:
use of possibly uninitialized variable: `currmin`
Более идиоматичный (и работающий) вариант этой функции выглядел бы так:
fn minval(A: &[int]) -> int {
A.iter().fold(A[0], |u,&a| {
if a<u {a} else {u}
})
}
struct A{
int *x;
A(int v): x(new int(v)) {}
~A() {delete x;}
};
int main() {
A a(1), b=a;
}
Собирается, однако падает при выполнении:
*** glibc detected *** demo: double free or corruption (fasttop): 0x0000000000601010 ***
То же самое на Rust:
struct A{
x: Box<int>
}
impl A {
pub fn new(v: int) -> A {
A{ x: box v }
}
}
impl Drop for A {
fn drop(&mut self) {} //нет необходимости, приведено для точной копии С++
}
fn main() {
let a = A::new(1);
let _b = a;
}
Собирается и выполняется без ошибки. Копирования не происходит, ибо объект не реализует trait Copy
.
Rust ничего за Вашей спиной делать не будет. Хотите автоматическую реализацию Eq
или Clone
? Просто добавьте свойство deriving
к Вашей структуре:
#[deriving(Clone, Eq, Hash, PartialEq, PartialOrd, Ord, Show)]
struct A{
x: Box<int>
}
#include <stdio.h>
struct X { int a, b; };
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,%dn", x.a, x.b);
}
Выдаёт нам:
2,2
Функция явно не ожидает, что ей передадут ccылки на один и тот же объект. Чтобы убедить компилятор, что ссылки уникальные, в С99 придумали restrict [5], однако он служит лишь подсказкой оптимизатору и не гарантирует Вам отсутствия перекрытий: программа будет собираться и исполняться как и раньше.
Попробуем сделать то же самое на 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 mutable
demo: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 ends
demo:7 swap_from(&mut x, &x);
^
demo:7:26: 7:26 note: previous borrow ends here
demo:7 swap_from(&mut x, &x);
Как видим, компилятор не позволяет нам ссылаться на одну и ту же переменную через "&mut
" и "&
" одновременно, тем самым гарантируя, что изменяемую переменную никто другой не сможет прочитать или изменить, пока действительна &mut
ссылка. Эти гарантии обсчитываются в процессе сборки и не замедляют выполнение самой программы. Более того, этот код сибирается так, как если бы мы на C99 использовали restrict
указатели (Rust предоставляет LLVM информацию об уникальности ссылок), что развязывает руки оптимизатору.
#include <vector>
int main() {
std::vector<int> v;
v.push_back(1);
v.push_back(2);
for(std::vector<int>::const_iterator it=v.begin(); it!=v.end(); ++it) {
if (*it < 5)
v.push_back(5-*it);
}
}
Код собирается без ошибок, однако при запуске падает:
Segmentation fault (core dumped)
Попробуем перевести на Rust:
fn main() {
let mut v: Vec<int> = Vec::new();
v.push(1);
v.push(2);
for x in v.iter() {
if *x < 5 {
v.push(5-*x);
}
}
}
Компилятор не позволяет нам это запустить, вежливо указав, что изменять вектор в процессе его обхода нельзя:
demo:7:13: 7:14 error: cannot borrow `v` as mutable because it is also borrowed as immutable
demo:7 v.push(5-*x);
^
demo:5:14: 5:15 note: previous borrow of `v` occurs here; the immutable borrow prevents subsequent moves or mutable borrows of `v` until the borrow ends
demo:5 for x in v.iter() {
^
demo:10:2: 10:2 note: previous borrow ends here
demo:5 for x in v.iter() {
demo:6 if *x < 5 {
demo:7 v.push(5-*x);
demo:8 }
demo:9 }
demo:10 }
#include <stdio.h>
enum {RED, BLUE, GRAY, UNKNOWN} color = GRAY;
int main() {
int x;
switch(color) {
case GRAY: x=1;
case RED:
case BLUE: x=2;
}
printf("%d", x);
}
Выдаёт нам «2». В Rust жы Вы обязаны перечислить все варианты при сопоставлении с образцом. Кроме того, код автоматически не прыгает на следующий вариант, если не встретит break
. Правильная реализация на Rust будет выглядеть так:
enum Color {RED, BLUE, GRAY, UNKNOWN}
fn main() {
let color = GRAY;
let x = match color {
GRAY => 1,
RED | BLUE => 2,
_ => 3,
};
println!("{}", x);
}
int main() {
int pixels = 1;
for (int j=0; j<5; j++);
pixels++;
}
В Rust Вы обязаны заключать тела циклов и сравнений в фигурные скобки. Мелочь, конечно, но одим классом ошибок меньше.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
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: %pn", (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("donen");
}
Порождает несколько ресурсов вместо одного:
done
resource: 0x7f229c0008c0
resource: 0x7f22840008c0
resource: 0x7f228c0008c0
resource: 0x7f22940008c0
resource: 0x7f227c0008c0
Это типичная проблема синхронизации потоков, которая возникает при одновременном изменении объекта несколькими потоками. Попробуем написать то же на Rust:
struct Resource {
value: Option<int>,
}
impl Resource {
pub fn new() -> Resource {
Resource{ value: None }
}
pub fn acquire<'a>(&'a mut self) -> &'a int {
if self.value.is_none() {
self.value = Some(1);
}
self.value.get_ref()
}
}
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 mutable
demo:20 let ptr = res.acquire();
Вот так может выглядеть причёсанный код, который удовлетворяет компилятор:
extern crate sync;
use sync::{Arc, RWLock};
struct Resource {
value: Option<Box<int>>,
}
impl Resource {
pub fn new() -> Resource {
Resource{ value: None }
}
pub fn acquire(&mut self) -> *int {
if self.value.is_none() {
self.value = Some(box 1)
}
&**self.value.get_ref() as *int
}
}
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: 0x7ff4b0010378
resource: 0x7ff4b0010378
resource: 0x7ff4b0010378
resource: 0x7ff4b0010378
resource: 0x7ff4b0010378
Понятное дело, что на С++ тоже можно написать правильно. И на ассемблере можно. Rust просто не даёт Вам выстрелить себе в ногу, оберегая от собственных ошибок. Как правило, если программа собирается, значит она работает. Лучше потерять полчаса на приведение кода в приемлимый для компилятора вид, чем потом месяцами отлаживать ошибки синхронизации (стоимость исправления дефекта [6]).
Rust позволяет Вам играть с голыми указателями сколько угодно, но только внутри блока unsafe{}
. Это тот случай, когда Вы говорите компилятору "Не мешай! Я знаю, что делаю.". К примеру, все «чужие» функции (из написанной на С библиотеки, с которой вы сливаетесь) автоматически маркируются как опасные. Философия языка в том, чтобы маленькие куски небезопасного кода были изолированы от основной части (нормального кода) безопасными интерфейсами. Так, например, небезопасные участки можно обнаружить в реализациях классов Cell
и Mutex
. Изоляция опасного кода позволяет не только значительно сузить область поиска неожиданно возникшей проблемы, но и хорошенько покрыть его тестами (мы дружим с TDD [7]!).
Guaranteeing Memory Safety in Rust (by Niko Matsakis) [8]
Rust: Safe Systems Programming with the Fun of FP (by Felix Klock II) [9]
Lang-NEXT: What – if anything – have we learned from C++? (by Bjarne Stroustrup) [10]
Lang-NEXT Panel: Systems Programming in 2014 and Beyond [11]
Автор: kvark
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/61668
Ссылки в тексте:
[1] обещанное: http://habrahabr.ru/post/224607/#comment_7639951
[2] online compiler: http://www.compileonline.com/compile_cpp11_online.php
[3] rust playpen: http://playtest.rust-lang.org/
[4] многостраничное сообщение об ошибке: http://codegolf.stackexchange.com/questions/1956/generate-the-longest-error-message-in-c
[5] restrict: http://en.wikipedia.org/wiki/Restrict
[6] стоимость исправления дефекта: http://superwebdeveloper.com/2009/11/25/the-incredible-rate-of-diminishing-returns-of-fixing-software-bugs/
[7] TDD: http://en.wikipedia.org/wiki/Test-driven_development
[8] Guaranteeing Memory Safety in Rust (by Niko Matsakis): https://air.mozilla.org/guaranteeing-memory-safety-in-rust/
[9] Rust: Safe Systems Programming with the Fun of FP (by Felix Klock II): http://www.techtalkshub.com/rust-safe-systems-programming-fun-fp/
[10] Lang-NEXT: What – if anything – have we learned from C++? (by Bjarne Stroustrup): http://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Keynote
[11] Lang-NEXT Panel: Systems Programming in 2014 and Beyond: http://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Panel-Systems-Programming-Languages-in-2014-and-Beyond
[12] Источник: http://habrahabr.ru/post/225003/
Нажмите здесь для печати.