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

Я не притрагивался к C++ с тех пор, как ещё в старших классах разрабатывал игры на Cocos2D-X, но решил, что сохранившихся у меня туманных воспоминаний о «правиле трёх» (или сколько там было? Пять? Ноль?) и прочих подобных материях будет более чем достаточно, чтобы решить такую задачу. Оказалось, что и мне требуется кое-что подучить, но я с удовольствием узнал, что существует большая аудитория, с которой можно поделиться этими знаниями. Почти любую концепцию из C++ легко понять, если объяснить её в ключе «о, эта как та штука из Rust».

Притом что C++ местами несимпатичен, этот язык по-своему красив. Я и так это знал, но когда взялся заново учиться C++, мне стало только яснее: если Rust в какой-то степени и превосходит C++ (допустим, вы верите, что это так), то лишь потому, что сам Rust стоял на плечах такого гиганта, как C++.

Так что мы потратили пару недель, проштудировав серию руководств по OpenGL от ютубера под ником TheCherno (кстати, сама серия отличная). Две недели спустя нам удалось отобразить на экране единственный статичный голубой квадратик. Я уже стал опасаться, а не начнёт ли моя сестра сомневаться, стоило ли таким образом изучать разработку игр и пытаться изобразить что-нибудь на C++. Так что тогда я решил, что следует отбросить руководства господина Черно и взяться за разработку игры всерьёз.

Мы приступили к реализации фич геймплея и принялись выделять их в примитивный «игровой движок». С гордостью скажу, что теперь у нас есть полноценная игра, можно скачать её и попробовать в неё поиграть  (кодовое название SpaceBoom). Когда мы в целом рассортировали весь код на «элементы движка» и «элементы игровой логики», сестра решила «порулить» — реализовать большую часть игровой логики самостоятельно! Я же «подносил патроны», когда она просила реализовать ту или иную фичу на стороне игрового движка.

Достоинства C++

Свобода

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

Я здесь немного паясничаю, но C++ в самом деле позволяет вам делать что вы захотите. Я в итоге написал этот ужасный шаблон для мемоизации типов, которые могут использоваться совместно, однако имеют RAII. В результате мне удалось учетверить кадровую частоту нашей игры, лишь минимально изменив тот код геймплея, который писала моя сестра. Не заостряю тут внимания на передаче чисел с плавающей точкой тем функциям, что ожидают целых чисел – правда, в конце концов накопились предупреждения компилятора, и нам пришлось править ошибки, из-за которых они возникали. А если нам требовалась глобальная переменная, то мы сначала просто делали её статической, а уже потом переходили к вопросам. Я достаточно хорошо владею Rust и усвоил, как именно в Rust делаются те или иные вещи. Поэтому, несмотря ни на что, могу быстро набросать черновик. Но я не завидую той альтернативной версии меня, в которой я просвещал бы сестру, что такое once_cell и Arc<Refcell<_>>.

Выразительность

Объектная ориентация также очень приятна. Знаю, все вокруг ненавидят объектную ориентацию, но она крайне хороша при написании игр. Создаём класс GameObject, затем объекты GameObject, представляющие собой квадраты, жёстко вписанные в плиточную карту. Далее относим квадраты к подклассу  SquareObject, делаем объекты SquareObject. Если эти объекты движутся, то относим их к подклассу Character — и т.д. Затем всё это помещается в один гигантский массив vector<unique_ptr<GameObject>>, после чего можно применять методы ->update() и ->render() с каждым из объектов. При таком подходе просто всякий раз вызывается нужная вещь, подход просто работает.

Ранее я успел активно попользоваться Bevy, игровым движком на Rust (см. automated-testing-in-bevy) и много могу о нём рассказать. Но Bevy позиционируется как «золотой стандарт» легкого в использовании паттерна ECS (сущность-компонент-система), а наш подход к написанию игры на C++ просто получился интереснее и продуктивнее. Немного постыдно, что реальный игровой движок оказывается для разработчика менее удобен, чем какой-то код на C++, который мы с сестрой сварганили за пару недель. Подчеркну, в начале этого проекта ни она, ни я как следует не ориентировались в C++. Это сравнение не совсем верное, поскольку Bevy отчасти жертвует продуктивностью разработчика за серьёзное повышение производительности системы; к тому же, движок очень быстро улучшается. Тем не менее, это заслуга C++, что мы смогли так быстро сделать готовый продукт, работая в своё удовольствие.

Отладка

Мне очень нравится, как обстоят дела с отладкой в Visual studio и VSCode. Не знаю, почему так, но кажется, что ничего подобного (по уровню) для Rust не существует. Даже если это по какой-то причине возможно, мне никогда не приходилось к таким возможностям обращаться. Мои Rust-проекты я всегда выполняю в Cargo или Bazel, и это явно сложнее, чем иметь возможность взять и нажать кнопку в IDE, потом просто пощёлкать мышкой и расставить контрольные точки, и чего вам ещё захочется. За всё время работы с Rust я пользовался отладчиком, может быть, однажды, вся остальная отладка шла через printf.

Современный C++

В C++ сохранилось множество старинных возможностей, которыми, как правило, следует пренебрегать в пользу эквивалентных фич из «современного С++», если только нет явно причины поступить наоборот (char *, массивы, указатели, malloc/free, NULL, т.д.)

Примерно как на этой классической картинке:

В данном случае лестно отметить, что современный C++ на самом деле очень приятен. Особенно unique_ptr , shared_ptr и std::array. Научившись пользоваться этими сущностями, можно, как правило, уже не беспокоиться о безопасности памяти. Конечно, по ходу разработки игры нам так или иначе придётся эпизодически сталкиваться с ошибками сегментирования, но такие ошибки легко поддаются исправлению.

Оговорюсь, что отчасти нам удалось в этом преуспеть благодаря моему опыту работы с Rust. У нас был вектор объектов GameObject, и мы хотели, чтобы все ссылки были действительны в пространстве всех кадров, поэтому обернули их все в unique_ptr (так мы получили возможность прикреплять к массиву новые элементы, не рискуя инвалидировать ссылки). Совершенно очевидно, что при прикреплении новых элементов к массиву имеющиеся ссылки приходят в негодность – полагаю, вы знаете, как реализуются векторы. Но я вполне представляю себе новичка, который об этом не знает и может столкнуться с такой проблемой при программировании.

Недостатки C++

Непростая судьба языка C++ к настоящему времени возвела его в поразительный статус: это язык, иерархия изменений в котором прослеживается вплоть до 1973 года, когда он был изобретён. Причём обратная совместимость нарушалась очень редко. Это настоящий технологический шедевр, и я не думаю, что было бы честно считать такую родословную недостатком C++. Да, из-за этого зачастую возникает путаница, но все альтернативы менее предпочтительны.

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

Сборка могла бы быть проще

Отсутствие стандартизированной сборочной системы реально напрягает. Сестра работает под Windows, а я – на макбуке. Поэтому в какой-то момент мы решили перейти с обычной Visual Studio на cmake. cmake — образчик хорошей инженерии и, может быть, я просто не умею пользоваться этим инструментом, но поверьте, что сочетать при работе cmake и Visual Studio совсем не так удобно, как пользоваться Cargo. В Visual Studio предусмотрен режим cmake, который работает нормально, но он явно выглядит второсортным по сравнению с реальной сборочной системой Visual Studio. Мы столкнулись с бесчисленными багами, что вынуждало нас то и дело закрывать и снова открывать инструмент (а в одном случае проблема решалась только через перезапуск компьютера).

Конечно же, C++ не виноват, что продукты Microsoft такие кривые, да и с cmake как-то можно перебиться. Но было бы здорово, если бы cmake хотя бы не усложняла лёгкие вещи, а разработчики пытались избегать проблем из разряда «ничего не знаю, у меня на машине работает». Есть и кое-что хорошее: в cmake предусмотрено отличное расширение для VSCode, работать с которым, по моему скромному опыту, весьма удобно.

Управлять пакетами не так просто

Поправка: мне уже рассказали о conan и vcpkg, которые, по-видимому, решают большинство проблем, затронутых в этом разделе. Однако оставлю его для потомков.

Сколько же времени тратится впустую из-за того, что в cmake не предусмотрен менеджер пакетов. Именно по этой причине возникала масса проблем из разряда «а у меня на машине работает». Кажется, что специалистам по C++ просто нравится устанавливать пакеты глобально, а затем добиваться, чтобы сборочная система сама как-то их нашла и связала. Но вот как мы поступим: я попытаюсь сделать у меня на макбуке какую-то операцию, под которую у меня определённо есть библиотека, а затем моя сестра попробует повторить это у себя – и код не соберётся. Я решил эту проблему окольным путём: просто собирал все элементы из исходного кода, насколько это возможно. Работа по сборке библиотечных зависимостей из исходного кода, по-видимому, должна строиться так: добавляете репозиторий как субмодуль git, настраиваете add_subdirectory и include_directories в cmake, затем добавляете библиотеку в target_link_libraries. Конечно, не всегда всё так просто, как описано здесь. Некоторые проекты, например glew, не предназначены для использования в качестве субмодулей git, поэтому только и остаётся, что впихнуть прямо в репозиторий простыни их кода. Кроме того, мне почему-то не было очевидно, какую именно библиотеку использовать в target_link_libraries (я потратил массу времени на glew, прежде, чем осознал, что его нужно связывать с glew_s, а не с glew). Уже не говорю о том, что в большинстве библиотек, которыми мы пользовались, нашлась папка для cmake, а в этой папке – файл readme примерно следующего содержания: «вот, какой-то чел добавил это через пул-реквест, и я, честно, не знаю, как это работает, но если хочешь – можешь попробовать». А если ваши зависимости увязаны с другими зависимостями – я по-прежнему не вполне понимаю, как это работает. В целом, работать так гораздо менее удобно, чем просто запустить cargo add $CRATE_NAME.

Также требовалось немного потрудиться, чтобы найти документацию — может быть, потому, что она не генерируется автоматически.

Сообщения об ошибках

Сообщения об ошибках в C++ (при использовании Clang и Visual Studio) также отличаются шероховатостью по сравнению с аналогами из Rust. Предположу, что именно из-за сообщений об ошибках моя сестра была так не склонна учить C++, если не касаться этой ситуации со сборочной системой.

Вот что происходит, если изменить сигнатуру функции в заголовке, а в файле .cpp этого не сделать – и наоборот.

FAILED: OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o

/usr/bin/clang++ -DGLEW_STATIC -I~/coding/SpaceBoom/OpenGL/vendor/openal-soft/include -I~/coding/SpaceBoom/OpenGL/src -I~/coding/SpaceBoom/OpenGL/vendor/glfw/include -I~/coding/SpaceBoom/OpenGL/vendor/glew/include -I~/coding/SpaceBoom/OpenGL/vendor/glm -I~/coding/SpaceBoom/OpenGL/vendor/soloud/include -I~/coding/SpaceBoom/OpenGL/vendor/xxHash/cmake_unofficial/.. -I~/coding/SpaceBoom/OpenGL/vendor/openal-soft/include/AL -iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk/System/Library/Frameworks -g -std=c++20 -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk -mmacosx-version-min=14.2 -pedantic -Wall -Wextra -Wcast-qual -Wdisabled-optimization -Winit-self -Wmissing-include-dirs -Wswitch-default -Wno-unused -Wno-cast-qual -Wno-unused-parameter -MD -MT OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o -MF OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o.d -o OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o -c ~/coding/SpaceBoom/OpenGL/src/Renderer.cpp

~/coding/SpaceBoom/OpenGL/src/Renderer.cpp:18:30: error: out-of-line definition of 'ResPath' does not match any declaration in 'Renderer'

const std::string& Renderer::ResPath(bool idk) {

Сестра постоянно попадала в подобные ситуации, а искать каждую ошибку в man не так здорово, как может показаться. Как только успеешь разобраться, что значат все термины, начинаешь действовать чётко и прямолинейно, но поставьте себя на место моей сестры, которая едва представляет разницу между «объявлением» и «определением». Нигде даже прямо не указано, что определение находится в Renderer.cpp, а объявления лежат в Renderer.h. Да, я знаю, что в силу самого устройства C++ невозможно обеспечить качественные сообщения об ошибках на все случаи, но кажется, что именно этот казус и другие «простые» случаи довольно легко довести до ума.

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


  1. JordanCpp
    20.09.2024 21:03
    +6

    это язык, иерархия изменений в котором прослеживается вплоть до 1973 года, когда он был изобретён.

    Я как самый объективный и не ангажированный программист из всей статьи, прикопаюсь конечно же к дате:)

    С++ начинает отсчёт с 1983. Точнее Бьярни начал в это время, франкештеить экспериментировать с С, внедряя разные штуки. Что в итоге породило самый лучший язык программирования Pascal С++.


    1. nikolz
      20.09.2024 21:03
      +5

      Около 1974 года Деннис Ритчи закончил работу над языком, который был следующим этапом эволюции после B и назвал его C.

      История C++ начинается в 1979 году, в рамках докторской диссертации Страуструп решил создать инструмент, который был бы производительным, но при этом обеспечивал абстракцию. Его первой разработкой стал C with Classes.

      В 1982 году он решил доработать C with Classes, чтобы язык мог работать над проблемой распределенных вычислений. Так появился C++.

      Изначально C++ не был полноценным языком. Это был пакет препроцессоров для языка C.

      Первый публичный релиз 1985 года, Cfront 1.0, был довольно приличным, но чтобы пользоваться им, надо было быть экспертом в C, чтобы не сломать свой же код.

      К 1987 году GCC 1.15.3 начал поддерживать C++, а в 1989 году появился Cfront 2.0, который был гораздо лучше первой версии пакета.

      В 1990 году был создан комитет ANSI C++, а в 1991 году – комитет ISO C++. В итоге появились C++98/03, затем C++11, затем C++14, C++17, C++20, и так далее.

      https://tproger.ru/articles/istoriya-cpp-s-1953-goda


  1. JordanCpp
    20.09.2024 21:03

    Две недели спустя нам удалось на экране единственный статичный голубой квадратик

    А вот на ps1 для этого требовалось пол года:) Вполне быстро.


  1. olivera507224
    20.09.2024 21:03
    +5

    За всё время работы с Rust я пользовался отладчиком, может быть, однажды, вся остальная отладка шла через printf.

    Вот тут я что-то не уловил. В чём заключается сложность расставить брейкпойнты и запустить отладку Раста нажатием одной кнопки в VSCode, как вы описали это для C++?


    1. JordanCpp
      20.09.2024 21:03
      +1

      Вы не понимаете, это другое:)


      1. olivera507224
        20.09.2024 21:03
        +2

        Другое - это, например, Кложур. Вот там отладка действительно при первом знакомстве вызывает взрыв головы)


        1. JordanCpp
          20.09.2024 21:03

          Я тут на ночь, погуглил кложур. Чёт сложно... Мозг отказывается понимать.


    1. vtb_k
      20.09.2024 21:03

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

      Совсем не улавливаете иронии в статьи? Вся статья же пропитана ею.


      1. SeanT
        20.09.2024 21:03

        Так это ирония и сатира, понял


    1. Medeyko
      20.09.2024 21:03
      +2

      Этот текст в основном - ненавязчивый подкол C++. В данном месте автор подразумевает, что в Rust'е типа отладка практически не требуется... (Ну, по моему опыту потребность в отладчике на Rust'е действительно гораздо меньше, и отладочного вывода часто достаточно там, где на C++ уже очень хочется отладчика.)

      Впрочем, надо отметить, что перевод текста не очень хороший, и он частично замыливает иронию и сарказм (не только по отношению к C++, но и к Rust'у, и к самому себе), достаточно неплохо поданные в оригинале.


      1. olivera507224
        20.09.2024 21:03
        +2

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


        1. KanuTaH
          20.09.2024 21:03
          +5

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