Pull to refresh

Сравнение Rust и С++ на примерах

Reading time8 min
Views36K

Предисловие


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

Все 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 <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 );
}

Представьте себе радость человека, читающего многостраничное сообщение об ошибке, если он создал такую ситуацию случайно.

Шаблоны в 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) ведёт себя точно так же, как и обычная переменная на стеке (удаляется при выходе из зоны видимости). Для совместного использования данных есть подсчёт ссылок (std::rc::Rc) и сборщик мусора (std::gc::Gc), оба реализованы как сторонние классы (Вы можете написать свои).

Потерявшийся указатель на локальную переменную


Версия С++:
#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, %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 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("%d\n", 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,%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 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 }


Опасный Switch


#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: %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");
}

Порождает несколько ресурсов вместо одного:
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 просто не даёт Вам выстрелить себе в ногу, оберегая от собственных ошибок. Как правило, если программа собирается, значит она работает. Лучше потерять полчаса на приведение кода в приемлимый для компилятора вид, чем потом месяцами отлаживать ошибки синхронизации (стоимость исправления дефекта).

Немного про небезопасный код


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
Tags:
Hubs:
+60
Comments96

Articles

Change theme settings