LINUX.ORG.RU

C++ — туда и обратно, или зачем нужен Boost

 , , ,


3

5

Мой предыдущий тред в Development собрал самое большое число ответов аж с сентября, то есть за последние 9 месяцев, и это лишний раз подтверждает упадок этого форума. Полагаю, кто-то должен это изменить, и поэтому мы с тобой, ЛОР, поговорим сегодня про C++.

Начиная с C++26 вместо std::function вводится пачка новых классов: std::copyable_function, std::move_only_function (доступна с C++23) и std::function_ref. Что же не так с оригинальным std::function, ты можешь спросить? А вот что:

#include <functional>
#include <print>

struct call_me {
    int x = 0;
    void operator()() {
        std::print("x was {}\n", x++);
    }
};

int main() {
    const std::function<void()> f = call_me{};
    f();
}

Несмотря на то, что переменная f объявлена константной (люблю оксюмороны!), у неё есть внутреннее состояние и оно меняется при вызовах. Компилятор это без проблем хавает.

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

Небольшой список фич, которые были придуманы, оказались не нужны/бесполезны/вредны и выкинуты:

  • Известный vector<bool>, живущий издревле в STL и про который все говорят, что его надо избегать. Частично заменяется std::bitset.
  • std::auto_ptr. Бесполезен, ломает контейнеры, выкинут на помойку в C++17.
  • Указание исключений у функции в формате throw(X, Y). Так же выкинуто в C++17.
  • std::iterator объявили устаревшим в C++17, собираются удалить в C++26.
  • std::aligned_storage и std::aligned_union добавлены в C++11, объявлены устаревшими в C++23, скоро удалят.
  • Ключевое слово register удалено в C++17, хотя всё ещё доступно в Си.
  • std::get_temporary_buffer и std::raw_storage_iterator удалены в C++20.
  • Потрясающее по эпичности фиаско с интерфейсом для сборщиков мусора. std::declare_reachable сотоварищи были добавлены в C++11. Выяснилось, что сборщики мусора для C++ писать либо никто не умеет, либо никто не хочет, поэтому в C++23 это всё удалили и сделали вид, что ничего не было.
  • Абсолютное безумие вокруг концептов, модулей и поддержки сети. Предложения одобряли, вновь отклоняли, переделывали, и по итогу теми же модулями до сих пор никто не пользуется.
  • Сопрограммы (coroutines). В том виде, в котором они есть в C++, это просто ужас. Достаточно того, что корутины требуют выделения памяти из кучи во время работы, а значит вообще не подходят для случаях, когда требуется серьёзная производительность. Например, в любом коде, требующим работы в реальном времени и не позволяющем делать системные вызовы.

Просто лютый трешак, который никто подчищать пока не собирается:

  • std::regex – лютый тормоз, рекомендуется не использовать.
  • Мертворождённый std::simd, добавленный в C++26 и уже с ходу не нужный вообще никому, потому что код с std::simd в два-три раза тормознее чем со сторонними библиотеками, просто голыми интринсиками, и даже медленнее чем просто цикл for.
  • std::async. Дескруктор ждёт завершения асинка и поэтому может залочить весь код тебе. Наконец починили в C++26, но эта штука была сломана 15 лет.
  • Отвратительно спроектированный <iostream>. Его наконец можно выкинуть и использовать std::print, но не все про это знают.
  • Абсолютно тормозные контейнеры map, set, unordered_map. Вместо первого можно использовать flat_map из C++23. Контейнеры в стандартной библиотеке Rust (BTreeMap) и другие реализации B-Tree Map их обгоняют по производительности, но тем не менее в C++ выбирают убогий дефолт.

Решения многих из этих проблем существуют в Boost и сделаны там гораздо лучше. Но в то же время возникает важный вопрос: зачем вообще нужен настолько плохо спроектированный язык, где каждое следующее поколение инженеров, работающих над ним, отменяет решения предыдущих, а код под новые стандарты часто нужно переписывать если не с нуля, то очень близко к тому? Даже процесс разработки Rust с его поехавшими клоунами в юбках на этом фоне выглядит адекватным.

В общем, всё печально, ЛОР. Такие дела.



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

Ну справедливо, луч от камеры — это вообще более честный подход, потому что камера у тебя и так уже держит view-projection матрицу, и ты просто инвертируешь то, что и так посчитано, вместо того чтобы городить отдельный unproject для UI-слоя.

Луч от игрока — это то же самое, только camera offset уже включён в позицию персонажа, и получается, что ты стреляешь не из глаз абстрактной камеры, а из глаз конкретного Васи с мечом, что для gameplay-логики честнее, особенно если у тебя есть third-person камера, которая от игрока в трёх метрах позади и слегка сверху, и тебе плевать откуда рендерится картинка, тебе важно откуда физически летит урон.

Про двух-проекционность — да, это стандартная штука, screen-space UI рисуется отдельным ортографическим проходом поверх перспективного, и они действительно не обязаны делить один raycast. Просто дело в том, что «нарисовать двух-проекционно» и «протестировать клик двух-проекционно» — это два разных пайплайна, которые легко рассинхронизировать: рендер у тебя честно рисует UI поверх сцены, а хит-тест по привычке лезет сначала в мировые координаты, потому что кто-то полгода назад один раз написал raycast для gameplay и все input-события с тех пор идут через него по инерции.

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

так самое простое, что можно придумать, вы это и писали раннее, сначала обходим УИ - 2д, и else тут пошел счет перенос в мировые координаты, из (камеры, сдвиги, глаза, и прочее ).

именно поэтому я написал, что сначала мы городим коробки, пускай и 1 кусок сцены, без стриминга, типо сделали квадрат коробок, сделали коллизии. и вот тут иногда просто не хочется, писать как есть, и приходиться писать УИ систему, и думать как это в архитектуру прокидывать, так вот из-за прохода луча и системы УИ, УИ просто красиво пишется отдельно, но как бы текущий узел мира, сидит на else проходе, типо никуда не попали попадаем в мир, так я о том и написал, в этой ситуации не важно какая игра 2д/3д. нам важно в УИ прокинуть - тоесть оповестить УИ в каком узле мира мы находимся.

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

Сравнение borrow checker и наследования — это категориальная ошибка, так как они решают абсолютно разные задачи. Один контролирует безопасность памяти и времена жизни ресурсов на этапе компиляции, а второе является лишь одним из способов реализации полиморфизма. В Rust полиморфизм строится на трейтах, что избавляет от проблемы хрупкого базового класса и жесткой связанности глубоких иерархий. К тому же принцип композиции вместо наследования сейчас активно применяется и в самом C++.

Что касается модулей: их отсутствие в Си — далеко не ключевая причина появления Rust. В C++20 модули уже есть, но проблемы безопасности памяти это не решило. Если добавить модули в Си, он всё так же останется языком с ручным управлением памятью и регулярными уязвимостями, так что потребность в безопасном системном языке никуда бы не делась.

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

Ну вот именно, ты же сам и сформулировал главный вывод, просто ещё не назвал его словом: это же Chain of Responsibility в чистом виде, только вместо паттерна из книжки у тебя просто if/else с двумя ветками, и это нормально — паттерн не перестаёт быть паттерном от того, что ты его написал по наитию, а не назвал по имени (a rose by any other name). Сначала UI-слой говорит «мимо», потом эстафета уходит в мир — и вот этот «мимо» и есть весь контракт, который тебе нужен, никакого стриминга, никакого ECS с нодами, просто честное else.

И вот тут ты нащупал ту самую вещь, из-за которой людям «не хочется писать как есть» — потому что UI-система формально не часть мировой архитектуры, но architecturally обязана знать, что мир вообще существует, хотя бы чтобы уметь сказать «я тебя не съела, лети дальше». И вот это вот «прокинуть узел мира в UI» — это как раз та точка, где 90% инди-движков начинают городить event bus, потому что тыкать UI напрямую в GameState руками — грязно, а формального контракта между слоями ещё нет, вот и получается observer, garbage под шумок, и три способа подписаться на одно и то же событие к концу проекта.

Но смысл у тебя верный: не важно 2D это или мерзкое 3D, потому что и там и там задача одна — «проверили UI, не попали, теперь сообщи миру, в каком узле мы оказались». Разница 2D/3D тут вообще не в логике прохода, а только в том, чем ты меряешь пересечение — AABB против ray-vs-sphere, суть от этого не меняется, меняется только математика внутри одной и той же ветки else.

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

это в срезе инструментов, наследование это инструмент, боров это инструмент.

С упрощает отводит от ассемблера, не давая наследование, - первый инструмент

С++ упрощает взаимодействие классов и отводит от С,

а боров решает возможно вопросы безопасности и скорость, но уводит от С и С++

модули тоже инструмент в С/С++, потомучто инклуд может напряч процессор. Получается это всё на плоскости инструментов по взаимодействию с чем-то в ПК.

Типо появился язык, который просто учел, нюансы безопасности и компиляции для скорости, давая семантику мув без брейн-тайм, но забирая взаимодействие классов своими нюансами в той же степени и он этим ближе к С, а не С++ как я вижу.

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

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

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

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

оптимизации лексического анализа

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

деконструкция самой методологии

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

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

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

самый первый тест у меня был это просто интерфейс на BVH, просто 2 узла, тут красивый хендл - он практически 1, просто есть 2 бинарных дерева мир и уи(тоесть тут как раз в пас посылаем контракт, контрактом служит тип прохода, потомучто пас 1, просто прогнать можно либо тип УИ либо сцену ), но тут проблема в том, что всякие скролы и прочее помойму не удобны для бинарного дерева), но тут надо знать, а парент-чилд, это я глянул, походу это самое ходовое, что реально работает.

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

Ну BVH для этого — элегантно как идея, потому что ты по сути говоришь «у меня два больших bounding volume, один называется UI, другой называется Мир, и я просто проверяю контракт до того, как лезть внутрь» — это честно, это быстро, и для двух верхних узлов это работает как часы. Проблема начинается ровно там, где ты сам и нащупал: скролл.

Скролл ломает BVH именно потому, что BVH рассчитан на то, что геометрия либо статична, либо перестраивается редко и дорого — ты же строишь дерево заранее, а потом только спускаешься по нему. А скролл — это контент, который живёт за пределами своего собственного bounding box'а, то есть у тебя есть видимый прямоугольник контейнера, а внутри него — контент, который формально снаружи, просто со clip-mask, и вот это «формально снаружи, но визуально внутри» убивает всю честность BVH, потому что твои bounds врут по определению. Ты либо делаешь bounds по полному контенту — и тогда хит-тест начинает видеть элементы, которые юзер не видит, либо по видимому rect'у — и тогда ты руками досчитываешь offset при каждом запросе, что уже не BVH, это BVH с костылём.

А парент-чилд тут выигрывает именно потому, что он не про геометрию — он про ownership и порядок обхода. Скролл в парент-чилд — это просто нода с clip-rect и scroll-offset, которая знает, что все её дети живут в её локальном пространстве, и при хит-тесте просто трансформирует точку клика в своё пространство перед тем, как спросить детей. Никакого rebuild, никаких честных bounds — просто рекурсивный спуск с трансформацией контекста, и это работает для любой вложенности, хоть скролл внутри скролла внутри попапа внутри скролла.

Так что твой BVH хорош на верхнем уровне — UI vs Мир, один пас, один контракт — а дальше внутри UI честнее именно парент-чилд, потому что UI это не про пространственную близость, это про иерархию владения, а это разные вопросы, просто оба выглядят как дерево.

shdown ★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария