В своей предыдущей статье [перевод на Хабре] я говорил о множестве недостатков C++, которые, по сути, устранил Rust. Благодаря этому код теперь легко использовать правильно и сложно использовать неверно. Я не говорил о безопасности по памяти, просто привёл пример того, что пользователь функции не может случайно поменять местами аргументы количества и цены.

На написание статьи меня вдохновил доклад Мэтта Годболта о том, как можно сделать интерфейсы C++ более надёжными: Correct by Construction: APIs That Are Easy to Use and Hard to Misuse. Вам стоит его посмотреть!

В той статье я сказал, что Rust гораздо лучше помогает разработчику, возможно, благодаря тому, что у него были десятки лет, чтобы учиться. В конце концов, первая версия C++ была выпущена в начале 80-х, а Rust — в начале 2010-х. Если дать C++ несколько десятков лет для обучения, то, разумеется, появятся новые структуры, которые будут обладать высоким качеством и которые сложно использовать неправильно.

Но так ли это?

Сегодня мы рассмотрим приведённый Мэттом в его докладе пример по предотвращению неправильного использования класса после деструктивной смены состояний. Приведу контекст: допустим, у нас есть класс для управления компиляцией шейдеров. Мы можем создать объект, добавить любое количество шейдеров, скомпилировать их, а затем запрашивать у объекта скомпилированные шейдеры:

#include <iostream>
#include <stdexcept>
#include <vector>

class CompiledShader {
public:
  explicit CompiledShader(std::string name) : m_name(name) {};
  std::string m_name;
};

class ShaderRegistry {
public:
  // может вызываться только при компиляции
  void add(const char *shader);

  // после добавления всех шейдеров
  // компиляция может быть вызвана только один раз
  void compile();

  // получаем скомпилированный шейдер по имени.
  // он должен быть скомпилирован и скомпонован!
  const CompiledShader &get_compiled(const char *name) const;

  std::vector m_names;
  std::vector m_compiled;
};

От показанного выше кода неприятно попахивает, и причина запаха кроется в комментариях. Он даёт понять, что эти «оправдывающиеся» комментарии — хороший показатель того, что этот API легко использовать неправильно.

Можно представить, что такой реестр может продолжать удерживать определённые сдампленные ресурсы или переместить владение ими после компиляции, из-за чего объект не будет подходить для нового этапа компиляции. Ещё важнее то, что пользователь может попробовать получить скомпилированный шейдер до того, как он будет скомпилирован. В качестве решения Мэтт предложил разбить этот класс на два:

class CompiledShaders {
public:
  explicit CompiledShaders(const std::vector &names);

  const CompiledShader &get_compiled(const char *name) const;

  std::vector m_compiled;
};

class ShaderCompiler {
public:
  void add(const char *shader);

  // Использованные в компиляции ресурсы
  // передаются в CompiledShaders:
  // нельзя вызвать compile() дважды!
  
  CompiledShaders compile() const;

  std::vector m_names;
};

Этот API гораздо красивее и понятнее! Теперь мы просто не можем вызвать get_compiled() до компиляции, потому что этот метод доступен только в объекте, который мы получаем благодаря компиляции. Однако всё равно остаётся пара проблем. У нас всё равно есть ShaderCompiler , который сохраняется после вызова compile(), поэтому ничто не мешает нам попробовать использовать его снова:

// Неправильно
    ShaderCompiler compiler;

    compiler.add("alice");

    auto registry = compiler.compile();

    compiler.add("bob");

    auto wat = compiler.compile();

    auto shader = registry.get_compiled("bob");
    std::cout << shader.m_name << std::endl;

С этим компилятор ничего не может сделать. Он не знает, как читать наши комментарии, и ничто в определении класса не реализует правила, не позволяющие вызвать add() после compile(), или выполнять двойной вызов compile(). Но мы наверняка сможем улучшить ситуацию! И Мэтт пытается это сделать.

На сцене появляется C++11

Настало время C++ проявить всю свою мощь. У него были десятки лет на исследование того, как проектировались другие языки, и почти три десятка лет учёбы на самом C++, так что у комитета были все возможности решить описанные проблемы. Он много лет работал над новой старшей версией стандарта, которую назвали C++11, потому что она была выпущена в 2011 году — всего за несколько месяцев до первого публичного релиза Rust.

В данном случае было бы замечательно, если бы мы могли выразить то, что объект был «перемещён» — это один из способов концептуального рассуждения о происходящем: мы перемещаем состояние из ShaderCompiler в CompiledShaders.

Это не только должно упростить нам рассуждения о коде, поскольку компилятор будет знать, с какого момента состояние объекта концептуально изменилось, но и помочь с производительностью, так как компилятор может пользоваться этим знанием и перемещать данные вместо копирования — мы получаем больше контроля за тем, что происходит в памяти, а это важный аспект для системных языков.

Знаете, что добавилось в C++11? Важная новая фича под названием move semantics. Её первый строительный блок — оператор &&, указывающий ссылку rvalue — если упростить, это ссылка на значение, которое должно быть уничтожено при завершении текущей конструкции. Поверх этого был добавлена std::move(), которая, как понятно из названия, перемещает... хотя нет, постойте, на самом деле ведь это не так.

На самом деле она преобразует тип того, что мы передаём && — ссылку rvalue. Ну ладно, тогда всё в порядке, ведь мы знаем, что ссылки rvalue позволяют сказать «это будет немедленно уничтожено, так что можно выполнить перемещение». Решение становится простым:

class ShaderCompiler {
public:
  void add(const char *shader) { m_names.push_back(shader); };

  
  CompiledShaders compile() && {
    CompiledShaders shaders = CompiledShaders(m_names);
    return shaders;
  }

  std::vector m_names;
};

int main(void) {
  // Правильно
  ShaderCompiler compiler;

  compiler.add("alice");
  compiler.add("bob");

  auto registry = std::move(compiler).compile();

  auto shader = registry.get_compiled("bob");
  std::cout << shader.m_name << std::endl;
}

Мэтт изменил определение compile(), чтобы оно стало «методом rvalue», добавив суффикс &&, и мы выполняем std::move() объекта при вызове, что теперь обязательно из-за этого суффикса метода. Замечательно. То есть теперь компилятор должен защитить нас от подобного ошибочного использования ShaderCompiler:

// Неправильно
  ShaderCompiler compiler;

  compiler.add("alice");

  auto registry = std::move(compiler).compile();

  compiler.add("bob");

  auto wat_bang = std::move(compiler).compile();

  auto shader = registry.get_compiled("bob");
  std::cout << shader.m_name << std::endl;

Мы вызвали compile() явно, вызвав std::move() для объекта. Повторное выполнение этого действия стало бы концептуальным эквивалентом use-after-free. Мы делаем всё намеренно и явно; компилятор должен забраковать этот код или, по крайней мере, начать громко жаловаться об использовании после перемещения. Вот, как выглядит вывод компилятора:

kov@couve > clang++ -std=c++23 -Wall -Wextra -Werror -Wpedantic move/move-3.cpp
kov@couve >

Всё верно — код скомпилировался без единого уведомления. Если покопаться, то можно узнать, что от всего этого можно добиться того, чтобы компилятор потенциально заметил возможность избежать копирования, но и это не гарантируется.

Вероятно, всё это не произойдёт, если вы разберётесь во всех тонкостях. Скорее всего, вы проделаете кучу работы, добавите несколько новых конструкторов, разбросаете по коду && и std::move(), но не получите от этого никакого выигрыша, продолжая иметь возможность использовать объекты после того, как они потенциально были освобождены оптимизацией move. Более того, бывали случаи, когда использование std::move() предотвращало применение оптимизации!

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

Мэтт упоминает, что инструмент статической проверки наподобие clang-tidy пожалуется на этот код, но на самом деле это должно быть работой компилятора, особенно в столь очевидном случае.

Когда я думаю о комитете C++, то представляю группу людей, усиленно старающихся сделать так, чтобы новые фичи было сложно использовать корректно и создать при этом максимальное количество ловушек. Это обеспечит для CppCon стабильный поток часовых докладов о том, что чаще всего всё работает не так, как ожидалось. Я сержусь не на Николая, а на то, что ему пришлось выступать с этим докладом!

Вы уже догадалась, что это главная причина моей неприязни к C++?

Как Rust улучшает ситуацию?

Откровенно говоря, чтобы улучшить подобную ситуацию, требуется не так много. Но здесь мы приступаем к хорошо известному аспекту Rust: его модели владения и заимствования. Когда вы ссылаетесь на значение, Rust явным образом заставляет показать, что с ним будет происходить.

Это очень легко увидеть, посмотрев на методы, которые мы добавляем к struct:

impl MyType {
  fn read_something(&self) -> Something { ... }
  fn modify_something(&mut self) { ... }
  fn consume(self) { ... }
}

Просто посмотрев на определения, вы получаете всю необходимую информацию о владении и типе доступа. read_something() получает обычную ссылку, называемую borrow (заимствованием), которая означает неисключительный, потенциально общий доступ без передачи владения. Это немного похоже на маркер const в конце объявления метода C++.

Также мы знаем, что возвращаемое Something — это значение, которым мы владеем, как вызывающая сторона, поскольку к нему не добавлено &. Оно наше и мы можем делать с ним всё, что нужно, в том числе и передать владение им какой-то другой функции или структуре данных. Если мы выйдем из области видимости, не сделав этого, оно освобождается.

В modify_something() мы используем изменяемую ссылку на self, также известную как mutable borrow; это означает, что функция имеет исключительный доступ — мы знаем, что других ссылок не существует, и это проверяет borrow checker во время компиляции. В этом случае владение тоже не передаётся.

Наконец, в consume() используется голый self. Это сообщает нам, как в возвращаемом значении Something, что функция теперь владеет значением, для которого она была вызвана, и которое будет уничтожено после завершения области видимости, если только функция не передаст или не вернёт его.

Стоит отметить, что это не что-то особенное, свойственное только методам и self. Любое голое значение или тип — это явное перемещение с передачей владения. Эти правила сильно упрощают нам создание необходимой защиты:

struct CompiledShaders {
    ...
}

impl CompiledShaders {
    fn get(&self, name: &str) -> Option<&CompiledShader> {
        ...
    }
}

struct ShaderCompiler {
    ...
}

impl ShaderCompiler {
    fn new() -> Self {
        ...
    }

    fn add(&mut self, name: String) {
        ...
    }

    fn compile(self) -> CompiledShaders {
        ...
    }
}

fn main() {
    let mut shader_compiler= ShaderCompiler::new();

    shader_compiler.add("alice".to_owned());

    let registry = shader_compiler.compile();

    // Неправильно!
    shader_compiler.add("bob".to_owned());
}

Так как в качестве аргумента для компиляции используется голый self, мы знаем, что значение, представленное shader_compiler, перемещается в эту функцию. Иными словами, владение передаётся в эту область видимости. После её завершения значение теряется и не может быть использовано. Как и можно ожидать, если мы попробуем использовать его после этого, то компилятор чётко сообщит нам, что это недопустимо:

error[E0382]: borrow of moved value: `shader_compiler`
  --> move/move-1.rs:57:9
   |
51 |         let mut shader_compiler = ShaderCompiler::new();
   |             ------------ move occurs because `shader_compiler` has type `ShaderCompiler`, which does not implement the `Copy` trait
...
55 |         let registry = shader_compiler.compile();
   |                                        --------- `shader_compiler` moved due to this method call
56 |
57 |         shader_compiler.add("bob".to_owned());
   |         ^^^^^^^^^^^^^^^ value borrowed here after move
   |
note: `ShaderCompiler::compile` takes ownership of the receiver `self`, which moves `shader_compiler`
  --> move/move-1.rs:30:16
   |
30 |     fn compile(self) -> CompiledShaders {
   |                ^^^^

error: aborting due to 1 previous error

Просто, явно, предсказуемо. Почему с семантикой move всё не так?

В заключение

На мой взгляд, именно в этом Rust превращается из хорошего языка в отличный. Именно благодаря этому вы как новичок начинаете получать удовольствие от мер защиты, а не бороться с borrow checker.

Мы, разработчики на C/C++, привыкли просто разбрасывать по коду указатели и самостоятельно проверять соответствие ограничениям на основании нашего понимания кода. Разумеется, это становится главным источником багов памяти и конкурентности. У современного C++ есть множество помогающих инструментов, но, как сказал Мэтт, вам нужно проактивно пользоваться ими, и пользоваться правильно. Работая на C, мы, по сути, предоставлены сами себе. Rust же заставляет нас намеренно формулировать всё так, чтобы это не просто было правильно, но и чтобы можно было доказать, что это правильно.

Обучение правильному структурированию кода и превращение борьбы с borrow checker в приятное сотрудничество с ним уже не относится к теме моей статьи. Но если вы находитесь на этом этапе своего пути, то крайне рекомендую прочитать книгу Learning Rust With Entirely Too Many Linked Lists. Моему мозгу программиста на C она замечательно подошла.

Есть различные видео потрясающего Йона Йенгсета, в которых глубоко разбираются многие из концепций. Также у него есть видео о реализации различных библиотек и приложений, которые очень удобны в качестве конкретных примеров того, как можно рассуждать об архитектуре. Разве не замечательно, что сегодня мы можем учиться на видео, в которых настоящие специалисты показывают свою работу и объяснения? Я был бы счастлив, если бы что-то подобное существовало, когда я начал изучать C в 1999 году. Это отличный способ упрочить свои знания после прочтения Rust Book.

В последней статье по мотивам доклада Мэтта я рассмотрю ещё один случай, с которым очень хорошо справляется Rust, но который поначалу кажется новичкам очень неуклюжим. Для этого случая в C++ есть очень хорошие инструменты; недостаток лишь в том, что их использование опционально. Поэтому обещаю, что следующая статья будет не такой грустной!

Наконец, должен сказать: комитет по стандарту C++, ну перестаньте. Соберитесь с мыслями. Вы способны на хорошее... но это? Это просто печально.

И если уж я начал: комитет по стандарту, пожалуйста, дайте нам качественный строковый тип и срезы с проверкой границ. По этому поводу есть замечательные идеи. Заранее спасибо.

Примечание об обычных ссылках Rust: многие люди называют этот тип ссылок read-only, но это не совсем так, и мы не можем точно знать, что в какое-то внутреннее состояние не были внесены изменения; на самом деле в модели есть разделение на общие и исключительные ссылки.

В Rust есть множество инструментов для изменения общего состояния. Один из способов его реализации interior mutability, позволяющее более тонко управлять тем, что может изменяться, часто перемещая гарантии исключительного доступа в среду выполнения. У Йона Йенгсета есть видео, в котором этот вопрос рассматривается очень подробно. На случай, если вы не знаете, скажу, что Rust вынуждает делать это намеренно и в явном виде.

Комментарии (91)


  1. Sm0ke85
    29.05.2025 11:26

    Ну не знаю.... Я как бы застал С++ еще когда его юзали как "Си с классами" и на данный момент С++ - это прям мегалодон, который умеет все, а где не умеет - там прогнется и все равно сможет, по ощущениям.

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

    И да язык С/С++ меняется долго, потому что легаси и много в каких областях он используется, да и компиляторов много, и запросов разработчиков много и т.д. (просто раст еще не такой большой мальчик)

    Так что я буду ждать с нетерпением С+=2)))))


    1. domix32
      29.05.2025 11:26

      Так что я буду ждать с нетерпением С+=2)))))

      cppfront и carbon как раз про это.

      cppfront фактически делает апгрейд плюсов зашивая все механизмы GSL в язык, добавляя модули, обратную совместимость с плюсами и избавляя его от всякой дичи, типа спирали типов. В итоге всё это транспилируется в какой-то свежий С++ и в таком виде компилируется уже как обычно.

      Про carbon не расскажу ибо у них компилятор не везде работает, но выглядит он в среднем не лучше какого-нибудь D.


      1. Siemargl
        29.05.2025 11:26

        Интересно, Карбон надо посмотреть.

        А в чем ваши претензии к D? (у меня свои, сравнить )


        1. domix32
          29.05.2025 11:26

          Скажем, я не слишком фанат managed языков. Отдельное фе к именованию/code style, которое мне сильно напоминает Java. Можно ещё немножко поурчать про обманутые ожидания от флага -betterC. С другой стороны язык смог впитать заметное количество модных-молодёжных фич из других языков, так что разработчикам языка можно поаплодировать. В проде я это использовать конечно же не буду.


          1. Siemargl
            29.05.2025 11:26

            Наговариваете - он не менеджед, а нейтив, только что со сборщиком мусора, но можно и без него местами, что редкость для яп (@nogc) .

            Ява в него транслируется, это да, все же он надмножество. Навязываемый код-стайл как то не видел, как и принудительного форматтера. Мог пропустить.

            БеттерС без библиотеки это как голый король) Но ниша есть - CLI и embed.

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

            Проще приспособиться к новым С++.


            1. domix32
              29.05.2025 11:26

              managed - это как раз языки с gc - кто-то управляет (manages) памятью в противовес ручному управлению (manual). вы просто пишите var x = blabla() и не думаете почистится ли память или нет. C#, Java, D, Python, Haskell - как раз из этой оперы. Можно включать /выключать всякие фичи языка и даже Java можно компилировать в aot/no gc, но зачем, если я могу не делая приседаний иметь все то же самое поведение по-умолчанию в C++/Rust/Odin/Zig. V на худой конец.


              1. Siemargl
                29.05.2025 11:26

                Да, менеджед есть и такое понятие.

                У меня немного другие акценты, потому в списке есть ещё Jai, Nim и ещё несколько на посмотреть и написать когда нибудь статью)

                Проблемы в том, что всё ещё сыро (


    1. Yuuri
      29.05.2025 11:26

      С — это процедурное программирование, а не функциональное. Функциональное — это про композицию, функции высшего порядка, ограничение побочных эффектов и прочие лямбды с монадами.


      1. Kelbon
        29.05.2025 11:26

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


        1. Yuuri
          29.05.2025 11:26

          В си есть структуры, поэтому он структурный. А в паскале есть процедуры, поэтому он процедурный. А в хаскеле есть классы (типов), поэтому он классный ^_^ (типа)


  1. nv13
    29.05.2025 11:26

    То есть автор хочет сказать, что API на Rust нельзя сделать неправильно? А на С++ - нельзя сделать корректным?)

    Мне кажется пример с API не очень удачен. Проблема, имхо, не в конкретном языке, а в общем подходе к API как только к интерфейсу без поведения.


    1. domix32
      29.05.2025 11:26

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

      Что-нибудь типа

      using Shader = /* */;
      ShaderRegistry{ /* */ };
      
      class ShaderCompiler final{
         std::vector<Shader> names;
         friend ShaderRegistry Compile(ShaderCompiler&& compiler);
         ShaderRegistry compile();
      public:
         // единственный доступный метод. Возвращает ссылку на себя,
         // чтобы можно было вызывать цепочкой .add("asd").add("qwe")
         ShaderCompiler& add(std::string_view name);
      };
      // можно поменять сигнатуру на какой-нибудь unique_ptr
      ShaderRegistry Compile(ShaderCompiler&& compiler) {
        // доступ к приватному методу есть только тут
        return compiler.compile();
      };

      Ну, а проверку borrow checker пока ещё в процессе запиливания/стандартизации, так что пока не дожили.


      1. Siemargl
        29.05.2025 11:26

        Не так. Автор хотел, чтобы после вызова compile() вернулся уникальный экземпляр ShaderRegistry, причем очистился и сам ShaderCompiler.

        Но пример неудачный, ибо потери производительности нет, и так будет RVO. А чистить надо было вручную.

        Точнее, RVO будет для примера из статьи, у Вас хуже - будет копирование.


        1. domix32
          29.05.2025 11:26

          не стал сильно заморачиваться со ссылками ради упрощения. Так-то там и каких-нибудь конструкторов перемещающих дорисовать и концептов может каких, а то и в unique_ptr обернуть, чтобы время жизни ограничить.


      1. Deosis
        29.05.2025 11:26

        Даже такой вариант не защищает от использования переменной ShaderCompiler после передачи в метод Compile.

        По идее компилятор должен выбрасывать исключение, если объект используется в перемещенном состоянии.


        1. domix32
          29.05.2025 11:26

          Именно поэтому я и написал, что bc в процессе запиливания/стандартизации. Также как и кучка прочих safety пропозалов разной степени ароматности.


    1. 9241304
      29.05.2025 11:26

      А что вы хотели от фаната руста, у которого задача просто обосрать плюсы? При желании можно было прям на поверхности найти кучу объективных недостатков у плюсов, которые не хотят фиксить, зато раз в 3 года выкатывают новые и новые эксперименты, которые хз когда появятся в реале (вот прямо тут жирный недостаток). Но автор решил пойти тупым путем. То ли плюсов не знает, то ли...


      1. Sm0ke85
        29.05.2025 11:26

        Я думаю автор просто в поиск Лучшего языка для себя, я когда-то также искал, перепробовал много их, часть из этих языков (специализированных) походу не выжило, но в итоге пришел к тому, что идеален тот язык, который легаси большое имеет, который стандартизован не Васей и КО, который задаче наиболее подходит (имеет достаточно библиотек для решения задачи, дабы велосипеды в ненужных местах не изобретать) и т.д. Я даже когда Питон появился, его пощупал - штука интересная, особенно для академических или мелких проектов, но к себе его в багаж тогда не стал брать - пусть подрастает, а там видно будет (время все расставит на свои места, есть подозрения что он останется на позициях крутого "скриптового" языка для администрирования чего либо, да для старта в программировании, несмотря на то, что библиотек для него конечно ваяют очень не мало))), собственно, по моему мнению, он тем и превлекателен, что можно минимальными усилиями что-то быстро накатать и проверить как это работает)


  1. IIopy4uk
    29.05.2025 11:26

    В С++ есть некая особенность, определяющая всё - это поддержка легаси кода.

    А Rust'у до появления легаси ещё надо дожить :)


    1. domix32
      29.05.2025 11:26

      А Rust'у до появления легаси ещё надо дожить :)

      Ну, он его уже примерно 10 лет копит т.к. 1.0 в 2015 вышел. Просто у него механизм резолва легаси несколько отличается от тех же плюсов. Примерно столько же времени у плюсов ушло от первоначального выпуска до выпуска ISO стандарта.


  1. Emelian
    29.05.2025 11:26

    Может, если бы у C++ было больше времени, он стал бы лучше?

    А он и так хорош!

    Что меня умиляет в статьях авторов-теоретиков, что они не любят демонстрировать собственные проекты, где подтверждаются высказанные мим тезисы. Зато про «сферического коня в вакууме» могут говорить долго и с упоением.

    Хорошо, возьмем собственные пет-проекты. Какой язык выбрать? Ведь, никто не принуждает и не навязывает своего авторитетного мнения. Только личные предпочтения. Для меня, это, однозначно, С++, по многим причинам. И даже, если результаты подвергаются критике (см. мои программы в https://habr.com/ru/articles/848836/ и http://scholium.webservis.ru/Pics/MediaText.png ), то это не повод переходить на другой язык, а, всего лишь, дополнительный интерес, искать решения на «старом, добром» С++. Например, главная претензия ко мне была в отсутствии «современного» интерфейса (имея в виду плоский интерфейс с чёрной темой, как, скажем, у Блендера). Ладно, посмотрел в этом направлении, нашел, разные варианты, с которыми разбираюсь. И, может быть, при достаточном желании, реализую его в следующих версиях.


    1. firehacker
      29.05.2025 11:26

      Что-то я непонял перескок на чёрный UI как у Блендера.


      1. Emelian
        29.05.2025 11:26

        Что-то я непонял перескок на чёрный UI как у Блендера.

        Ну, я имел в виду интерфейс типа: https://erfaren.narod.ru/Pics/Blender.png . Или под «современным» интерфейсом понимается другой? Например, такой: https://erfaren.narod.ru/Pics/DuiLib.png ?


        1. firehacker
          29.05.2025 11:26

          Так какая связь между стилистикой интерфейса, претензиям к интерфейсу и выбором ЯП?


          1. Emelian
            29.05.2025 11:26

            Так какая связь между стилистикой интерфейса, претензиям к интерфейсу и выбором ЯП?

            Никакой, естественно. Я говорю не о связи, а о своей реакции на претензии. Народ хочет «чёрную тему»? Ладно, смотрим, что можно предложить. Варианты есть, в рамках ранее выбранного С++. Пока, разбираюсь. Какие вопросы?


  1. dersoverflow
    29.05.2025 11:26

    я говорил о множестве недостатков C++

    ай, ну кто так ругает? детский сад.

    30 years of single-minded search for uncompromisingly efficient and generic algorithms ended in shameful failure!

    https://www.linkedin.com/pulse/do-you-still-trust-stl-sergey-derevyago-bzenf


  1. AndreyFr
    29.05.2025 11:26

    Из за того что раст менее гибкий то и приложения на нем тормознутее, не?


    1. Siemargl
      29.05.2025 11:26

      Нет. Ограничение программисту даёт больше свободы [как обычно тупенькому] компилятору.

      В теории, да и кстати на многих задачах benchmarksgame, Rust выигрывает. Если убрать сишные трики с интринсиками итп


      1. AndreyFr
        29.05.2025 11:26

        Ага. Чем более ограничен программист - тем быстрее программы получаются?)


        1. Siemargl
          29.05.2025 11:26

          Эксклюзивность владения в Расте даёт сильные козыри для оптимизатора.


          1. Kelbon
            29.05.2025 11:26

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


            1. Siemargl
              29.05.2025 11:26

              Напомню про сишный restrict, введенный исключительно для этой цели


              1. Kelbon
                29.05.2025 11:26

                так restrict не по умолчанию и там где это контракт функции, явно указываемый. А в расте это неявно и очень легко получить уб

                Да и вообще, странно говорить "раст хороший - он убрал всё плохое что было в С++" и потом говорить "смотрите, у нас также как в С" (в С++ restrict нет)


                1. Dooez
                  29.05.2025 11:26

                  Ну вроде как получить уб как раз не тривиально потому что сложно получить мутабельную ссылку пересекающуюся с другой ссылкой.


                1. qwerty19106
                  29.05.2025 11:26

                  Но restrict по умолчанию только в safe Rust. А в unsafe мы оказываемся ближе к С (но не на его уровне), и можно писать код на указателях, главное при переходе снова к ссылкам соблюдать ограничение restrict.

                  На практике за этим надо следить разработчикам библиотек, которые что-то оптимизируют через unsafe и указатели, а в прикладном коде unsafe практически не встречается.


                  1. Siemargl
                    29.05.2025 11:26

                    в прикладном коде unsafe практически не встречается.

                    Это только в пропаганде. В соседней статье про редактор, написанный в Микрософте на Расте есть ссылка на гитхаб. Посчитай сам количество ансейфов.

                    И такая картина везде, где я смотрел


                    1. qwerty19106
                      29.05.2025 11:26

                      Посмотрел. Похоже, что его транслировали из C в Rust (например этим), и потом добились того чтобы компилировался.

                      Я делаю такой вывод из того, что внутри написана реализация memset, memchr и юникода (лол). Так что ваш пример - это антипример.

                      P.S. Вот пример +- большого проекта (https://github.com/picodata/picodata), изначально написанного на Rust. На 112kloc Rust кода 238 строк unsafe (0.2%). Из них около 80% это плагин к tarantool, т.к. обращения к ffi.


                      1. Siemargl
                        29.05.2025 11:26

                        Не похоже по коду, что это транспиляция. И об этом бы было написано, авторские производные права итп, корпорация же автор, строго.

                        memset просто там оптимизирован под SIMD, а юникод - биндинг к ICU (тут ансейф естествен)


                      1. qwerty19106
                        29.05.2025 11:26

                        Я вас прошу доказать вот эти ваши слова:

                        И такая картина везде, где я смотрел

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

                        А я вам привел большой контрпример на 112kloc. Теперь, если вы стоите на своем, докажите на нескольких (больших) проектах.


                      1. Siemargl
                        29.05.2025 11:26

                        А зачем доказывать? Я же не утверждаю, что везде так.

                        Токио я смотрел, а что ещё так и не упомнишь. Где то всплыло в обсуждениях - ткнул полистал.


                      1. qwerty19106
                        29.05.2025 11:26

                        Как и ожидалось, фразу про пропаганду вам подкрепить нечем.

                        Неужели так сложно сказать "я погорячился / я ошибся"?


                      1. Siemargl
                        29.05.2025 11:26

                        А, так вот о чем речь.... Так первичное утверждение было Ваше

                        в прикладном коде unsafe практически не встречается.

                        Вам его и доказывать.

                        Я привёл первый попавшийся контрпример)

                        А на Ваше "покажите большие проекты" я скажу - а проверим методом Монте-Карло.


                      1. qwerty19106
                        29.05.2025 11:26

                        Легко. Цитирую:

                        As of May 2024, there are about 145,000 crates; of which, approximately 127,000 contain significant code. Of those 127,000 crates, 24,362 make use of the unsafe keyword, which is 19.11% of all crates. And 34.35% make a direct function call into another crate that uses the unsafe keyword. 6 Nearly 20% of all crates have at least one instance of the unsafe keyword, a non-trivial number.

                        Most of these Unsafe Rust uses are calls into existing third-party non-Rust language code or libraries, such as C or C++. In fact, the crate with the most uses of the unsafe keyword is the Windows crate 7, which allows Rust developers to call into various Windows APIs.

                        Примерно 81% крейтов не содержат ни одной строчки unsafe. А среди остальных большинство - это FFI.

                        Теперь ваша очередь доказывать про пропаганду. Надеюсь получить аргументированный ответ.


                      1. Siemargl
                        29.05.2025 11:26

                        Отлично, т.е 80% крейтов не нуждаются в ансейф. А поскольку речь шла о прикладном по, а не библиотеках, то в этой части должно быть и больше.

                        Теперь проверим моим способом. Открываем awesome rust, и из нейтральных приложений - не системных, не ffi, не игр, берём несколько наугад.

                        Например субд паритидб, смотрелка CSV, текстовый редактор, веб фрейворм поем

                        Я с телефона толком не поищу, смотрите самостоятельно много ли там ансейфа и обоснован ли он


          1. AndreyFr
            29.05.2025 11:26

            если руки неумелые, а если умелые?

            ----

            Вообщем, можешь доказать, что програ в расте при одинаковом размере всегда быстрее за сишную?


  1. Siemargl
    29.05.2025 11:26

    А вся статья получается чистой спекуляцией на неоднозначности термина и поведения std::move()

    В отличие от Раста, в плюсах мув семантика только семантика и не приводит к исчезновению переменной.

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


    1. ZirakZigil
      29.05.2025 11:26

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

      Плюсовый мув ничего не делает с временем жизни. Вообще, всё, что делает std::move это кастит в &&.


  1. Jijiki
    29.05.2025 11:26

    могу ошибаться пример с шейдером не понял,

    значит есть статика, чтобы её рисовать, есть партиклы, есть анимации, 3 типа шейдеров, тоесть поидее должно быть 3 шейдера и вроде извините получается и на Алису и Боба 1 и тот же шейдер, потомучто надо просто пропустить модельку чтобы отрисовать и посветить на неё от игрока и источников, а ну еще шейдер сцены может быть (локация), + еффект(тоже тип шейдера), зачем на Боба и Алису создавать 2 разных шейдера, или я чото не понимаю

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

    даже если они разные всё равно поидее 1 шейдер, туда вроде даже воду(морфом) засунуть можно-как еффект на локации и тапать её по индентификатору(но прогоняя как воду тоесть уже категория получается, но шейдер то 1 вроде)

    тоесть другие категории это выбор елемента(уникальный тип взаимодействия), отрисовка 2д(новый тип отрисовки), ну и партиклы да наверно не засунуть в мейн шейдер, но 1 шейдером можно пол локации вроде отрисовать, при условии что он собирается перед компиляцией(софтовый include, это исключение повторов тогда внутри такого шейдера будет условие выбора отрисовки по типу, времени или еще как)


  1. Panzerschrek
    29.05.2025 11:26

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


  1. dv0ich
    29.05.2025 11:26

    Опять пиарщики Раста воюют с недостатками плюсов 15-летней давности


    1. Yuuri
      29.05.2025 11:26

      Отлично, то есть спустя 15 лет они всё-таки были исправлены! Исправлены, да?..


      1. dv0ich
        29.05.2025 11:26

        Достоинство Раста в том, что он не даёт наделать ошибок при работе с памятью. Откуда возьмутся такие ошибки в С++, если на нём писать как на С++, а не как на С?

        Обычная пиар-статья Раста это "давайте рассыпем по коду сырых указателей да наворотим дикой дичи в стиле pure C".


        1. eao197
          29.05.2025 11:26

          Откуда возьмутся такие ошибки в С++, если на нём писать как на С++, а не как на С?

          Да хотя бы из-за невнимательности программиста. Сохранить где-то ссылку на временную переменную, которая протухает при выходе из скоупа -- как два байта. Вы запросто это можете получить даже на обычном std::max.


        1. qwerty19106
          29.05.2025 11:26

          А вы точно прочитали статью? В конце раздела На сцене появляется C++11 ровно один указатель, и можно было бы заменить на std::string без изменения сути примера.

          А суть в том, что на повторный std::move компилятор не выдает ошибку (и даже варнинг), и нужно много приседать, чтобы компилятор что-то заметил. Ито это не гарантируется.

          "давайте рассыпем по коду сырых указателей да наворотим дикой дичи в стиле pure C".

          Статью не читали, но осуждаете да?


          1. ZirakZigil
            29.05.2025 11:26

            что на повторный std::move компилятор не выдает ошибку

            А почему должен? Стандарт не запрещает move-from больше одного раза.


            1. qwerty19106
              29.05.2025 11:26

              Я считаю что это ошибка дизайна стандарта, и автор статьи тоже пытается это показать на примерах.

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


            1. domix32
              29.05.2025 11:26

              Потому что повторный move приводит к UB, что может быть использовано в качестве уязвимости. Не слишком обнадёживающее поведение.


              1. ZirakZigil
                29.05.2025 11:26

                С чего бы оно должно к нему приводить? Стандарт прямым текстом говорит, что moved-from объект остаётся в unspecified but valid state (emph. mine). move из объекта, находящегося в валидном состоянии, сам по себе не UB.

                Не, оно конечно возможно (и даже совсем-совсем не сложно) написать такой код, который будет UB при двойном муве, но это нужно сделать специально. Не делайте :)


                1. domix32
                  29.05.2025 11:26

                   Не делайте :)

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

                  (и даже совсем-совсем не сложно

                  ну и вот это как раз и проблема - есть ненулевой шанс написать такой код случайно.

                  сам по себе не UB.

                  специально не расшифровывал UB, т.к. их два штуки - unspecified behaviour и undefined behavior. Первое как раз про двойной мув.


                  1. Siemargl
                    29.05.2025 11:26

                    специально не расшифровывал UB, т.к. их два штуки - unspecified behaviour и undefined behavior. Первое как раз про двойной мув.

                    Двойной мув допустим, просто в зависимости от применения, можно получить второй раз пустой объект.

                    Unless otherwise specified, all standard library objects that have been moved from are placed in a "valid but unspecified state", meaning the object's class invariants hold (so functions without preconditions, such as the assignment operator, can be safely used on the object after it was moved from):

                    Прямая цитата, наверное правильнее будет.


        1. tunegov
          29.05.2025 11:26

          std::string_view<char> foo(std::string str) { return str; }

          Вот идиоматичный C++11 код, который приводит к use after free.


          1. ZirakZigil
            29.05.2025 11:26

            Тут нет ничего идиоматичного (не говоря уж про то, что это не C++ код (и не потому, что UB)).


            1. eao197
              29.05.2025 11:26

              не говоря уж про то, что это не C++ код

              И какой же это код? Неужели Си-шный?


              1. ZirakZigil
                29.05.2025 11:26

                И какой же это код?

                Неправильный, вестимо

                И не сишный тоже.


                1. eao197
                  29.05.2025 11:26

                  Неправильный, вестимо

                  Т.е. если код написан на C++, но оказался неправильным, то это не C++ код?

                  Ой, как удобно!


                  1. Siemargl
                    29.05.2025 11:26

                    И даже смешно, что критик не осиливает 1 строчку кода без ошибок написать =)

                    Вообще есть в стандарте - the program is ill-formed; no diagnostic required.


                    1. tunegov
                      29.05.2025 11:26

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


                    1. eao197
                      29.05.2025 11:26

                      И даже смешно, что критик не осиливает 1 строчку кода без ошибок написать

                      Про какую именно ошибку речь? Про то, что нет string_view<char>?
                      Ну так в комментариях никто не обязан сходу писать компилирующийся и линкующийся код. Можно считать, что там написано либо:

                      std::basic_string_view<char> foo(std::string str) { return str; }
                      

                      либо

                      std::string_view foo(std::string str) { return str; }
                      

                      суть это не меняет.

                      Вообще есть в стандарте - the program is ill-formed; no diagnostic required.

                      В стандарте много чего есть, но как-то оно не сильно помогает писать код без ошибок.


                      1. Siemargl
                        29.05.2025 11:26

                        Чтение документации и учебников таки помогает писать код без ошибок.

                        В каком-то учебнике есть положительный пример возврата стрингвью?

                        Я, например, могу ткнуть пальцем в МС лёрн, где написано, как его применять.


                      1. eao197
                        29.05.2025 11:26

                        Чтение документации и учебников таки помогает писать код без ошибок.

                        Только вот это необходимое, но недостаточное условие.

                        В каком-то учебнике есть положительный пример возврата стрингвью?

                        А вы програмируете только так, как написано в учебнике?

                        Однако, у вас есть нехорошая привычка не отвечать на вопросы. А именно:

                        Про какую именно ошибку речь?


                      1. Siemargl
                        29.05.2025 11:26

                        std::string_view было бы правильно для компилятора .

                        Но неправильно применено.

                        Зачем писать очевидные ответы ?


                      1. eao197
                        29.05.2025 11:26

                        std::string_view было бы правильно для компилятора .

                        Ну вот как раз std::string_view<char> -- это неправильно для компилятора, т.к. std::string_view -- это псевдоним для std::basic_string_view<char>, этот псевдоним не может быть чем-либо параметризован.

                        Но неправильно применено.

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

                        И да, это как раз таки ошибка памяти, хоть вы так и не считаете:

                        Если начать с начала этой ветки комментариев, или даже самой статьи,то можно заметить, что ошибок памяти тут нет.

                        После таких заявлений мнение "экспертов" можно сходу отправлять в /dev/null.

                        Зачем писать очевидные ответы ?

                        Как раз в данном случае было более чем не очевидно. Но стало очевидно, что в предмете вы не разбираетесь, однако мнение имеете.


                      1. Siemargl
                        29.05.2025 11:26

                        Во-первых. Стрингвью притащили в комментариях и в другой ветке. В статье его нет. Upd. Ладно, ветка эта же.

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

                        Упд, ладно, я тоже ветки попутал. Взаимное недопонимание.


                      1. eao197
                        29.05.2025 11:26

                        Во-первых. Стрингвью притащили в комментариях и в другой ветке.

                        Ну так посмотрите к какому именно комментарию был дан ответ с примером со string_view: https://habr.com/ru/articles/913702/comments/#comment_28373696
                        Увидите, что к тому самому, в дочерних к которому мы сейчас и находимся.

                        В статье его нет.

                        А никто и не говорил, что он есть в статье.
                        Некто @dv0ich заявил буквально следующее:

                        Откуда возьмутся такие ошибки в С++, если на нём писать как на С++, а не как на С?

                        И ему дали пример того "откуда возьмутся". Я бы и сам примеров накидал, но было лень в 100500-ый раз повторять очевидное.

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

                        Правда? Да что вы говорите!
                        https://godbolt.org/z/G1fKf9ss1 -- покажите пальцем на ошибку от компилятора, плиз.


                      1. Siemargl
                        29.05.2025 11:26

                        main.cpp:7:10: warning: address of stack memory associated with parameter 'str' returned [-Wreturn-stack-address] { return str; } ^~~

                        Годболт с телефона глючит, это в cpp.sh, там по умолчанию Warnings

                        •  Many (-Wall)

                        •  Extra (-Wextra)

                        •  Pedantic (-Wpedantic

                        Но это без - fanalyzer

                        Про ветку комментов я ошибся, исправил)


                      1. eao197
                        29.05.2025 11:26

                        Ну вот вам еще godbolt для gcc 14.3 с -Wall -Wextra -Wpedantic: https://godbolt.org/z/q7TEdvc3x
                        Не ругается.

                        Аналогично с wandbox: https://wandbox.org/permlink/rYq6VFom5zNtmzYt

                        Нет ни ошибок, ни предупреждений.

                        Более того, реальных проектов, где использовались бы -Wall и -Wextra не так уж и много. Просто потому, что в реальных проектах используются сторонние либы, а их авторы не заморачиваются на то, чтобы код компилировался без предупреждений на высоких уровнях. Да и слишком накладно достижение этого, особенно если нужно поддерживать и gcc, и clang, и msvc. Достаточно подключить в проект asio или fmtlib, чтобы получить кучу предупреждений при наличии -Wall.

                        Ну и главное: пример-то тривиальнейший, не так уж сложно сделать пример, в котором наивный анализ от компилятора ничего не выявит.


                      1. Siemargl
                        29.05.2025 11:26

                        Расследование показало, что cpp.sh использует clang, и на годболте тоже ошибку он видит.

                        Насчет ошибок памяти тут я не согласен ни с шапкозакидательским ни с Вашим утверждениями.

                        По моему, в С++ давно идёт борьба со скрытыми программисткими ошибками в области надежности, но граблей остаётся предостаточно.

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

                        И все равно, компилятор вас спасать не особо собирается.


                      1. eao197
                        29.05.2025 11:26

                        Расследование показало, что cpp.sh использует clang, и на годболте тоже ошибку он видит.

                        И? Есть широко используемый компилятор, который не видит. Сделать пример, в котором и clang не будет видеть не проблема от слова совсем.

                        По моему, в С++ давно идёт борьба со скрытыми программисткими ошибками в области надежности, но граблей остаётся предостаточно.

                        Вы это автору тезиса про "Откуда возьмутся такие ошибки в С++, если на нём писать как на С++, а не как на С?" объясните.

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

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

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

                        Что касается якобы моего утверждения, то оно относилось только к тому, что C++ не обещал решить все проблемы работы с памятью из Си.


                      1. Siemargl
                        29.05.2025 11:26

                        Мне кажется, Вы слишком категорически трактуете моё выражение "граблей остаётся предостаточно" и "сокращение количества ошибок" как полное избавление от них =) итд

                        Раст на мой взгляд ближе к тому подходу, что "если запретить автомобили, ДТП не будет"


                      1. eao197
                        29.05.2025 11:26

                        Мне кажется, Вы слишком категорически трактуете

                        Нет, я просто более чем толсто намекаю на то, что ваше высказывание "И даже смешно, что критик не осиливает 1 строчку кода без ошибок написать" мягко говоря не соответствует действительности.


                      1. Siemargl
                        29.05.2025 11:26

                        Это касалось только данного конкретного случая, не обобщая.


            1. tunegov
              29.05.2025 11:26

              А как должен выглядеть идиоматичный для данного примера?

              Проблема в том что до сих пор C++ это по большому счету сахар над C, как здесь, отдельно std::string и std::string_view написаны на языковых конструкциях C++, но связываются они по сути через C модель работы с памятью.


              1. Siemargl
                29.05.2025 11:26

                А как должен выглядеть идиоматичный для данного примера?

                Проблема в том что до сих пор

                В необразованности проблема.

                Идиоматические примеры написаны в учебниках. Приятной учёбы.

                Остальную чушь Я даже комментировать не буду, см п1


        1. Yuuri
          29.05.2025 11:26

          С++ — это, конечно, существенный шаг вперёд по сравнению с С, но всех проблем работы с памятью он не решает. Например, с инвалидацией итераторов или многопоточным доступом. Иначе и Rust не появился бы.


          1. Siemargl
            29.05.2025 11:26

            Ну всех проблем никто не решает.

            Везде какой то баланс цены, удобства за частичное решение.


            1. Yuuri
              29.05.2025 11:26

              Мой комментарий был ответом на вопрос «откуда возьмутся ошибки памяти в С++, которые не даёт наделать Rust» из предыдущего.


              1. Siemargl
                29.05.2025 11:26

                Если начать с начала этой ветки комментариев, или даже самой статьи,то можно заметить, что ошибок памяти тут нет.


                1. Yuuri
                  29.05.2025 11:26

                  Мой комментарий всё ещё был ответом @dv0ich


          1. eao197
            29.05.2025 11:26

            С++ — это, конечно, существенный шаг вперёд по сравнению с С, но всех проблем работы с памятью он не решает.

            Так он и не собирался это делать.


            1. Yuuri
              29.05.2025 11:26

              См. комментарий, на который я отвечала.


              1. eao197
                29.05.2025 11:26

                Я знаю на что вы отвечали и именно поэтому указываю на то, что C++ и не обещал решить проблемы по работе с памятью из Си.