> C-подобный синтаксис требует _минимум_ специальных обёртокЭто верно только до тех пор, пока ты ограничиваешь себя теми техниками программирования, которые требуют минимум специальных обёрток в C. Загляни в glibc, там есть, например, функция sort. Эта функция работает вызывая функцию сравнения элементов с передачей этих элементов указателями. Если тебе нужно быстро сортировать, то неплохо было бы функцию сравнения элементов внедрить в код сортировки -- это позволит оптимизатору сделать всё сильно лучше. Но в C это невозможно без специальных обёрток, которые привнесут в C программирование с generic типами как в C++. Альтернатива -- писать реализацию sort под каждую комбинацию типа контейнера и типа элемента. Можно ещё попробовать макрос запилить, для генерации таких реализаций, но макроязык в C немногим лучше sed'а, ради такой мелочи лучше его не использовать.
Хочешь ещё один пример? Я разочаровался в C лет десять назад, когда задумался о том, что в C нет пристойного механизма обработки ошибок, и попытался изобрести что-нибудь вменяемое. Всякие там non-local exits, типа longjmp и тому подобные, я не считал вменяемыми, и решил что раз есть возвращаемое значение, то в нём можно передавать информацию об ошибке. Но отказываться от того, чтобы возвращать return'ом из функции результат работы, и возвращать его присвоением указателю, мне тоже не хотелось. В результате я пришёл к чему-то в стиле:
enum error_t {
NO_ERROR = 0,
// тут перечисление всяких других констант
}
struct my_func_returns {
int ret;
error_t err;
}
struct my_func_returns my_func(...) {
...
return (struct my_func_returns){ .ret = 42, .err = NO_ERROR };
}
Это работало офигенно, машинный код получался прямо как задумано, такой как я бы на ассемблере сделал: функция возвращала два значения в двух регистрах, в одном возвращаемое значение, в другом код ошибки. Но это порождало такое количество boilerplate, то я пришёл к выводу, что это не вариант. Я пытался придумать что-нибудь с макросами, чтобы хотя бы на вызывающей стороне получалось что-нибудь пристойное, чтобы обработка ошибок не перемешивалась бы с основной логикой. Но в C совершенно убогий макроязык, он работает очень локально, не въезжает в синтаксис с которым работает, и поэтому тупо навтыкать на каждое возвращаемое значение по метке, и после каждого вызова функции делать проверку и goto на метку -- это не вариант: меткам всё равно имена надо назначать вручную, и всё равно получается куча формальной писанины, которая нужна только потому, что мне моча в голову ударила работать с ошибками по каким-то правилам.
В C++ же и в Rust это делается _элементарно_. Вместо объявления бесчисленного множества структурок типа my_func_returns, мы можем объявить тип навроде хаскелловского Either (в C++ это std::expected, в Rust -- это std::result::Result) и теперь мы можем не парится обо всех этих объявлениях структур my_func_result:
C++:
expected<int, error_t> my_func(...) {
...
return 42;
}
Rust:
fn my_func -> Result<i32, Error> {
...
Ok(42)
}
В C++, в Haskell, в rust в стандартных библиотеках есть уже готовые типы для комбинации возвращаемого значения и ошибки в возвращаемый тип, но если бы их не было, написать их не представляет никаких проблем. В C такой подход возможен, но непрактичен, без специальных обёрток, поэтому ни в какой стандартной библиотеке его не встретишь, и сам использовать не будешь: проще переключится на C++, и писать на C с классами^W^W с std::expected.
> относительно хорошо _машинно_ оптимизируется
Относительно чего он о хорошо оптимизируется? Все эти UB лезут в C как раз потому, что он плохо оптимизируется. Он хорошо оптимизировался под PDP-11, программист писал на высокоуровневом ассемблере; компиляторы, стремясь к максимальной оптимизации делали только то, что укладывается в принцип наименьшего удивления (least surprise principle). Но по мере изменения аппаратных платформ он стал оптимизироваться хуже, и разрыв между наивными ожиданиями программиста и реальностью всё увеличивается и увеличивается. На полном серьёзе люди ломают копья в спорах о том, должен ли оптимизатор следовать принципу least surprise или принципу максимальной оптимизации.
> позволяет при _достаточной грамотности_
Но несколько десятилетий развития IT показывают, что средний уровень грамотности постоянно снижается. Программирование, как род деятельности, постоянно снижает уровень требований -- теперь программируют не профессора, и даже не научные сотрудники, а люди, которые в лучшем случае осилили получить техническое образование. Заточка языка на какую-то там подразумеваемую грамотность -- это приговор языку.
rust требует некоторой грамотности, чтобы одолеть borrow checker, но фишка в том, что если грамотности нет, то ты borrow checker не одолеешь. У тебя будет два пути -- либо обрести необходимую квалификацию и забороть borrow checker, либо расчехлить unsafe и обойти borrow checker. Первое хуже наличия грамотности только тем, что требует времени, в течение которого твоя продуктивность будет близка к нулю. Второе же -- палево, потому как детектится элементарным: find . -name '*.rs' -exec grep -H unsafe {} \;