LINUX.ORG.RU

C++ vs Rust: правда ли, что Rust тупо сложнее крестов в базовых сценариях применения?

 


0

6

Если C++ разраба заставить писать код на определённом подмножестве C++ (в первом приближении: не выделять память руками, не юзать указатели, не кастовать типы), то в принципе течь и падать там будет негде. На деле чуть тоньше и проще: указатели можно, но если тебе его передали в конструктор. Есть циклические ссылки, но тоже можно чё-то придумать. В общем, чёткого набора рецептов нет, опытный разраб в конкретном проекте выработает свои достаточно рабочие, плюс статический анализ и прочие там sanity-check тулзы и valgrind-ы скажут где насрано автоматически.

А есть просто Rust, где достаточно запретить писать unsafe и всё будет гарантированно блестяще и даже думать не надо.

Но говорят, Rust сложнее. Думать там надо уже просто чтобы базово взлететь, тогда как для базового взлёта на C++ достаточно быть тупорылым сишником, которому запретили выделять память. Гонят?

! ! ! ААААА ПРОСЬБА ПЕРЕНЕСТИ В TALKS ОШИБСЯ ФОРУМОМ ! ! !

Дополнение к уже написанному в треде.

  1. Тяжёлое наследие плюсов: его нет, если его не юзать. Я же не пишу в Rust ассемблерные вставки везде. Возможность их написать же не говорит о том, что у Rust тяжёлое наследие всей x86/ARM аппаратной платформы. Нормальный C++ код не содержит никаких макросов, например и передач указателей в пределе. Передай ты std::span, std::string_view и т.п. вместо (char* ptr, uint32_t size).

  2. Никогда не понимал тезис, что синтаксис обычного Си - сложный. Он может быть сложный во всех ВОЗМОЖНОСТЯХ, но в базовых сценариях он кажется примитивным: int function_name(int a, int b) { return a * b;} - это же предельно тупейшая идея синтаксиса, которую придумает любой школьник при наличии задачи изобретения ЯП. Даже конфиги хочется писать в таком стиле - см nginx. Так вот, если сознательно не усложнять себе жизнь, то C++ так же прост.

  3. Посмотрим на такие конструкции Rust, выдернутые из контекста:

languages.get_statistics(&input, &cli.ignored_directories(), &config);

Имеются какие-то &. Не знаю что это, но почему не написано input вместо &input? То есть, юзера заставляют думать про разные виды передачи аргумнетов чтоли? Ссылочно/указательно? Чем это отличается от необходимости в крестах думать про rvalue, lvalue, reference, pointer? То есть, от этого момента язык тоже не ушёл: нельзя как в JS/Python херануть объект в аргумент и зашибись - надо думать как херануть.

(0..10).map(|_| "#").collect::<String>()

Питонячно. Какой-то генератор с вызовом какой-то лямбды на каждый объект генератора? Не в курсе как это точно работает, но питонячно! В современных плюсах подобное тоже выразимо, но это уже всё равно не уровень начинающего: понимание подобного что в плюсах, что в расте - признак не дебила.

fn main() -> Result<(), Box<dyn Error>> {

В С++ проще. Не надо писать fn, чтобы сказать, что это функция, достаточно привычных миру () и тип возвращаемого значения в C++ необязательно предварять -> чтобы сообщить компилятору, какое оно. Ту же мы видим некие генерики/шаблоны - в плюсах они выглядят так же.

let mut is_sorted = false;

В Rust это выглядит НАДЁЖНЕЕ чем в С++, потому что заставили написать mut, чтобы сообщить, что это можно менять. В C++ это выглядит так же коротко в принципе: auto is_sorted = false;. Но в крестах ты пишешь const auto is_sorted = false; если надо конст и всё.

Посмотрим как пилятся сруктуры в расте:

pub struct CodeStats {
    /// The blank lines in the blob.
    pub blanks: usize,
    /// The lines of code in the blob.
    pub code: usize,
    /// The lines of comments in the blob.
    pub comments: usize,

Блин, в C++ же проще:

struct CodeStats {
  // The blank lines in the blob.
  usize blanks;
  // The lines of code in the blob.
  usize code;
  // The lines of comments in the blob.
  usize comments;

Я потратил меньше кода в крестах. Мне не надо писать pub напротив каждого поля, я могу его вынести в начало. Плюс, struct в крестах - это по-дефолту всё pub, а class - по-дефолту всё private - можно регулировать приватовость всех полей сразу просто выбором слова, которым объявлять структуру.

Что бесит в Rust: тип в конце. Но в языках, где важна производительность, люди любят подумать про memory layout - «как всё лежит в памяти» и посмотреть в первую очередь на типы всего, что лежит в структуре: какой тип рядом с каким, как это выровняется, например. Понятно, что в структуру в таких случаях данные пихают не по выравниванию, а «что рядом с чем потребляется процом», чтобы «нужное вместе» в одну кеш-линию, поэтому важнее будут имена полей, чем типы. Но всё-таки хочется «от общего к частному»: сначала видеть ЧТО ЭТО В ПРИНЦИПЕ (какой у этого тип), а уже потом как оно называется. Условно, мне хочется в «семантике общения» ситуацию «это собака, её зовут Вася и это тоже собака, и гоша», а не наоборот: «это Вася, а ещё он собака, а это петя и он собака». Я не хочу думать про имена, я хочу сначала схватывать суть уровня «так, тут у нас две псины, что они тут делают», а как они называются я потом разберусь)

pub fn summarise(&self) -> Self {

Об этом уже говорилось, в C++ тут будет меньше кода. А где тип аргумента, статически типизированные вы наши, йопт? В целом понятно, почему они заставили писать pub перед каждой функцией - чтобы тупорылый разраб сразу видел точно публичное оно или нет. А то в C++ напишут слово public: а дальше ряд функций и могут случайно написать функцию не в той секции и она случайно будет public – лучше пусть явно пишут! Но хз, это вкусовщина: мне приятнее организовывать всё именно как в плюсах: написать public и дальше у нас красиво пошёл публичный интерфейс. Нафиг мне pub в глаза пихать на каждый чих. Та же конструкция в C++: Self summarise(T &self) {. Опять же, уже говорилось: наличие & - раст оказался не таким уж простым, юзеру надо думать ссылка там или не ссылка? Где такая же простота, как в ссаном JS, что просто self написал и всё?

Давайте просто сюда посмотрим: https://github.com/sharkdp/fd/blob/master/src/filesystem.rs – в принципе да, всё читаемо, красиво, выразительно. map всякие там. Отсутствие скобок у if бесит конечно, ну ладно, в питонячке так же. Но в целом код на современных крестах выглядит абсолютно так же, может чуть меньше символов напечатать придётся. А где-то вместо map().blabla().bubu() будет несколько процедурных строк, потому что в именно стандартную библиотеку C++ не подвезли именно такой семантики, но она достижима в самом языке.

Посмотрим сюда: https://github.com/XAMPPRocky/tokei/blob/master/src/input.rs

Спецсимвольный perl-адок какой-то немного. Зачем так жить. Степень жести в районе строк 16-26 вообще ничем не способна привлечь юзера в сравнении с самыми мерзкими местами C++. Ясно, что это всё можно как-то объяснить. Точно так же «как-то» можно объяснить вот такой C++ код: https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/src/c++11/thread.cc#L235 - синтаксис обычный такой «сишный», просто вперемешку с макросами и большим количеством ___ в названиях переменных: в общем, оба фаната «как-то» объяснят ад в своих языках, простой мане, которая хотела «простой язык» оба этих места одинаково жопные.

Хочется обратить внимание на такой тонкий психо-нюанс, который похоже имеет место быть в расте: раст далеко не питонячка, всмысле в нём уже таки надо думать о неком таком критическом количестве вещей, что требования к мозгу кандидата поднимаются на такой уровень, где ему уже совсем не противен C++ и возможные дедлоки в тредах не кажутся чем-то сложным: ой, опять мьютексы не в том порядке захватил, поправил и забыл.



Последнее исправление: lesopilorama (всего исправлений: 4)
Ответ на: комментарий от lesopilorama

нужно исключения бросать просто, а не null/false возвращать. это похапешная привычка. не даром его считают вредным (правда он вредный по историческим причинам). нужно с java/python пример брать

rtxtxtrx ★★
()
Ответ на: комментарий от dimgel

Сложнее, не сложнее… В расте нет ООП и исключений —> сразу на помоечку. Я, знаете ли, комфорт люблю.

Всмысле. То есть, в расте нельзя напилить некий ТИП/класс, снабдить его методами/функциями, а потом на его базе создать дочерний, переопределив поведение какого-то метода?

lesopilorama
() автор топика
Ответ на: комментарий от rtxtxtrx

нужно исключения бросать просто, а не null/false возвращать.

Исключения работают на порядок(в лучшем случае) медленнее, чем возврат значения.

PS: Правда я может не в ту ветку влез, вы там похоже не про плюсовые исключения говорите.

Loki13 ★★★★★
()
Последнее исправление: Loki13 (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

Если ты хочешь показать весь ужас раста, то надо приводить эквивалентные куски кода на каком-то другом языке, который по твоему мнению лучше. А так можно скатиться в какой-нить баян char *( *(*var)() )[10] x;

andalevor ★★★
()
Ответ на: комментарий от Loki13

я про js. там исключения надо бросать, а не всякое говно возвращать по похапешной привычке (где 99% функций возвращают null или false в случае ошибки)

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 1)
Ответ на: комментарий от Loki13

Не, исключения в C++ работают тупо как вызов 1 функции. На самом деле двух друг в друге, но не суть, это достаточно быстро - сравнимо с твоим if (err) { call_process_error(); } но не надо ловить код ошибки на каждои выходе и писать if.

lesopilorama
() автор топика
Ответ на: комментарий от lesopilorama

Я как-то парсил csv диких размеров и поначалу, по глупости, кидал исключения, вместо возврата false, на кривые данные. Не мог понять, почему так тормозит, заменил на возврат значения и всё сразу забегало. Да, там кидались миллионы исключений, т.к. данных было сотни миллионов ячеек.

Loki13 ★★★★★
()
Последнее исправление: Loki13 (всего исправлений: 1)
Ответ на: комментарий от Loki13

Я наврал немного, C++ исключение может отработать в 10 раз медленнее, чем if(), потому что side-таблицы не в кеше и надо поразматывать стек, НО клбчевой прикол в том, что если исключение не произошло, то это бесплатнее, чем наличие if (false == result)...

Если у тебя ошибка - это нечно требующее нетривиально её обработать: запись в лог, послать HTTP 500, поосвобождать ресурсов и происходит реже чем 50% случаев вызова, то исключения офигенно предпочтительнее: на то, что разматываться стек бцдет в 10 раз медленнее, чем один if всем пофиг на фоне того, что эту ошибку таки надо еще как-то пообрабатывать, на что уйдет ещё 100500100500 тактов. Зато если ошибки нет, то все бесплатно. Наличие throw - zero cost, наличие try/catch - zero cost.

lesopilorama
() автор топика
Ответ на: комментарий от lesopilorama

А потом ты используешь либу, подключая ее хэдерами, в которых нигде не указано, бросает ли этот код исключения и какие. В итоге получаешь поведение, которое может тебя удивить.

nemec784
()
Ответ на: комментарий от nemec784

используешь либу

Не понял. Если я использую какую-то либу и выбора нет, то ясен хрен мне придется смириться со всем, что в ней происходит, при чем тут исключения. Если либа прекрасна, но от нее воняет, то что делать, другой всё равно нет.

lesopilorama
() автор топика
Ответ на: комментарий от lesopilorama

Речь идет о способе возврата ошибок. Либо они возвращаются вместе с результатом вызова (а-ля Result в Rust или Err в Go), либо путем бросания исключения.

nemec784
()
Последнее исправление: nemec784 (всего исправлений: 1)
Ответ на: комментарий от archie

а с чем ты сравниваешь этот пропозал? У раста например ничего подобного даже близко нет: плачут, колются, компиляют по полчаса и фигачат процедурные макросы, которые по сути внешняя кодогенерация вроде жинжа-шаблонов на питоне.

Lrrr ★★★★★
()
Ответ на: комментарий от LamerOk

Не, если цель возвращаемого значения «норм/ненорм», то if не нужен. В 99.999% случаев все норм и проверять ничего не надо. Ненормовость кинется исключением, если она прлизойдет.

lesopilorama
() автор топика
Ответ на: комментарий от Lrrr

а с чем ты сравниваешь этот пропозал?

Сравниваю чисто по синтаксису с рандомным куском растокода. Ибо исходный посыл был именно про синтаксис, а не семантику. Уродский синтаксис крестов с кучей нечитабельных закорючек абсолютно ничем не лучше аналогичного уродского синтаксиса раста. В этом плане обоим до того же питона как до луны пешком.

archie
()
Ответ на: комментарий от rtxtxtrx

либо что намного реже React

На React сейчас тоже многие используют TypeScript и типизацию. Особенно на больших проектах. Даже к старым проектам подключить его не то, чтобы сложно, а эффект огромный.

и как какой-то дегенерат вместо строки может число в функцию передать

Легко. Особенно когда это некоторые айдишники, которые по природе своей числа. Причём в 99% случаев всё будет в порядке, и ты даже можешь передать этот id в другие функции. А сломается всё когда-нибудь или при передаче параметра на строго типизированный бэк, или при строгом сравнении в том же js.

про трушное ООП вместо суррогата на прототипах, а как только в JS завезли нормальное ООП

И какое же «нормальное» ООП завезли в JS? Которое синтаксический сахар поверх тех же прототипов?

static_lab ★★★★★
()
Последнее исправление: static_lab (всего исправлений: 1)
Ответ на: комментарий от Bfgeshka

Неправда, GC нет, течь и падать очень даже может и без С указателей

Поясни, от чего спасает GC при утечках не из-за C указателей. Ты про циклические ссылки? Редко сталкивался на практике с утечками из-за циклических ссылок.

urquan
()
Последнее исправление: urquan (всего исправлений: 2)
Ответ на: комментарий от Loki13

Т.е. ты сначала пытался использовать исключения не по назначению, а потом пришёл к выводу «какое говно эти ваши исключения». О чём кроме твоей неопытности это должно говорить?

rupert ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Rust решает не только проблему с нулевыми указателями, но и с многопоточностью в том числе.

Безопасность ЯП != безопасность ПО-решений (комментарий)

Автор Wayland композитора Way Cooler переписывает своё детище с Rust на C (комментарий)

gag ★★★★★
()

возьмём один из базовейших сценариев - конечные автоматы, нужны на всех уровнях айтишечки, желательно типобезопасные особенно для системщины, на плюсах находится что-нибудь такое(https://github.com/qlibs/sml):

// events
struct connect {};
struct established {};
struct ping { bool valid{}; };
struct disconnect {};
struct timeout {};

int main() {
  // guards/actions
  auto establish = [] { std::puts("establish"); };
  auto close     = [] { std::puts("close"); };
  auto reset     = [] { std::puts("reset"); };

  // states
  struct Disconnected {};
  struct Connecting {};
  struct Connected {};

  // transitions
  sml::sm connection = sml::overload{
    [](Disconnected, connect)      -> Connecting   { establish(); },
    [](Connecting,   established)  -> Connected    { },
    [](Connected,    ping event)                   { if (event.valid) { reset(); } },
    [](Connected,    timeout)      -> Connecting   { establish(); },
    [](Connected,    disconnect)   -> Disconnected { close(); },
  };

  static_assert(sizeof(connection) == 1u);
  assert(connection.visit(is<Disconnected>));
  assert(connection.process_event(connect{}));
  assert(connection.visit(is<Connecting>));
  assert(connection.process_event(established{}));
  assert(connection.visit(is<Connected>));
  assert(connection.process_event(ping{.valid = true}));
  assert(connection.visit(is<Connected>));
  assert(connection.process_event(disconnect{}));
  assert(connection.visit(is<Disconnected>));
}

корявенько и мутно со всеми этими visit, overload и лямбдами, и это ещё современные плюсы и это для них максимум выразительности . Более менее прямой аналог на расте:

enum Event {
    Connect,
    Establ,
    Ping(bool),
    Disconnect,
    Timeout,
}
#[derive(Debug, PartialEq)]
enum State {
    Disconnected,
    Connecting,
    Connected,
}
impl State {
    fn process_event(&mut self, ev: Event) -> &Self {
        use Event::*;   use State::*;
        *self = match ev {
            Connect => {
                est();
                Connecting
            }
            Establ => Connected,
            Ping(valid) => {
                if valid { reset(); };
                Connected
            }
            Timeout => {
                est();
                Connecting
            }
            Disconnect => {
                close();
                Disconnected
            }
        };
        self
    }
}
pub fn main() { 
    let mut conn = State::Disconnected;
    assert!(conn == State::Disconnected);
    assert!(conn.process_event(Event::Connect) == &State::Connecting);
    assert!(conn.process_event(Event::Establ) == &State::Connected);
    assert!(conn.process_event(Event::Ping(true)) == &State::Connected);
    assert!(conn.process_event(Event::Disconnect) == &State::Disconnected);
    println!("sizeof = {}", size_of::<State>());
}

компилится https://godbolt.org/z/EK5eKGcso примерно в тоже самое (смотреть main() ) , и это без доп. библиотек из коробки, по ясности- красивости даже в питоне лучше не получиться, если только в хачкеле каком-нибудь. И это ещё самое простое для рачта, если по полной расчехлить растовый тайпстейт вменяемый аналог на плюсах вообще не получится написать

zurg
()
Последнее исправление: zurg (всего исправлений: 1)
Ответ на: комментарий от zurg

Ты пропустил всю суть крестового примера - декларативную табличку переходов, и выдал на гора обычный свитч. На масштабах три состояния + четыре перехода разница глаз не сильно режет. На больших цифрах твой свитч (как и любой другой свитч) превратится в плохо поддерживаемую портянку.

LamerOk ★★★★★
()
Ответ на: комментарий от LamerOk

Ты ведь понимаешь, что эта «табличка переходов» — это тупо пачка лямбд, котоая работает за счёт перегрузки? У тебя есть входное выражение, набор паттернов, сопоставляемых с типом входного выражения, и связанный с каждым паттерном исполняемый код + возвращаемое значение.

То есть это абсолютно изоморфно match{} в расте, и «декларативности» там ровно столько же. Вот оверинженеринга меньше, да.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 3)
Ответ на: комментарий от intelfx

Вот ты точно не понимаешь, что вся крестовая мутотень сделана целиком и полностью ради таблички. Без таблички она нахер не нужна. Альтернативный вариант на расте должен в итоге предоставлять схожий пико-DSL, где в табличной форме можно оформить всю матрицу «начальное состояние» * «событие» - «результирующее состояние».

это абсолютно изоморфно

Свитчу, ага. Спасибо, кэп.

«декларативности» там ровно столько же.

Протри монитор - в примере выше один большой и толстый свитч на всю программу с телом обработчика на каждый кейс. В крестоподелке вообще нет свитча. Я же русским языком написал:

больших цифрах твой свитч (как и любой другой свитч) превратится в плохо поддерживаемую портянку.

Смасштабируй оба примера на 20 состояний с переходом из каждого в каждое и прикинь, во что превратится растовая простынка.

LamerOk ★★★★★
()
Ответ на: комментарий от LamerOk

лол что, вся это канитель из visit, overload, шаблонов и такой то матери, это как раз попытка изобразить алгебраические типы с паттерн матчингом, которые в рачте by design, про степень декларативности выше написали уже

zurg
()
Последнее исправление: zurg (всего исправлений: 1)
Ответ на: комментарий от LamerOk

не никакого в расте свитча, конструкции такой в языке даже нет,

Смасштабируй оба примера на 20 состояний с переходом из каждого в каждое и прикинь, во что превратится растовая простынка.

ровно одинаково превратится, но рачтовый хотя бы читабельней будет

zurg
()
Ответ на: комментарий от LamerOk

столько же сколько и плюсах? в примере же совпадает, или в плюсах астральный хацкерный канал какой-то открывается, который магически, без дополнительной писанины, свяжет события с действиями если их больше 5?

zurg
()
Ответ на: комментарий от LamerOk

Начнём с простого: покажи мне в плюсовой портянке «табличку». Потому что я вижу только одномерный список (State, Event) -> State { actions }, который полностью изоморфен match{}. На N состояний и M событий у тебя в этом несчастном массиве лямбд будет N×M строк (за вычетом несуществующих комбинаций).

Т.е. в плюсах в буквальном смысле переизобретают паттерн матчинг, делая его из механизма перегрузки (в духе троллейбуса из буханки). Только вот незадача, в расте он есть в самом языке.

Но как бы да, то, что ты называешь match{} свичом, *уже* автоматически дискредитирует тебя в этом разговоре ¯\_(ツ)_/¯

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 2)
Ответ на: комментарий от intelfx

Начнём с простого: покажи мне в плюсовой портянке «табличку». Потому что я вижу только одномерный список

Потому что ты смотришь в книгу, а видишь фигу. Крестовый пример сделан ради этого:

sml::sm connection = sml::overload ...

На N состояний и M событий у тебя в этом несчастном массиве лямбд будет N×M строк

Верно. Они спокойно будут лежать себе в одном файле в одной табличке на 20, на 100, на 1000 строк. Эту табличку можно прочитать, можно понять, можно выверить переходы, можно проверить вызываемые методы, можно сверить со спекой, если вдруг такая есть. Всю остальную простыню можно разбить на разные файлы и распихать по разным углам. Вся остальная простыня не имеет значения в контексте конечных автоматов.

то, что ты называешь match{} свичом, автоматически дискредитирует тебя в этом разговоре

То, что ты не понимаешь, что конечные автоматы сами по себе == switch, вообще лишает тебя права голоса в этой дискуссии.

LamerOk ★★★★★
()
Последнее исправление: LamerOk (всего исправлений: 1)