Порівняння 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 <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<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, %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
Функція явно не очікує, що їй передадуть Посилання на один і той же об'єкт. Щоб переконати компілятор, що посилання унікальні, в С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
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.