• Какая разница на практике между clang и gcc?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Однажды я на подобный вопрос уже отвечал, правда там вопрос был о кроссплатформенности, а не о трансляторах.

    Какая разница между clang и GCC.
    Разница большая. GCC обладает обширной поддержкой наследия идиом и конструкций из языка C, которые, вынужденно или по своей воле, поддерживает в современном C++.
    VLA, тип по умолчанию, всевозможные изыски синтаксиса C. Это все GCC не глядя принимает за C++ код и позволяет трансляцию.
    GCC даже сегодня многократно нарушает стандарты C++ просто потому что выбрал стратегию поддержки экзотической функциональности C в коде C++. Так же GCC не хвастается и скоростью поддержки стандартов C++.
    В 2016 году Google полностью отказались от поддержки GCC в Android NDK из-за слишком плохой поддержки стандартов и слишком свободного следования стандартам C++. В этот момент GCC стал неконкурентоспособным относительно оставшихся двух самых широко используемых трансляторов.
    Clang же, наоборот, сегодня считается, буквально, бастионом идеального следования стандартам C++. Clang точно поддерживает стандарты во всех деталях, максимально быстро интегрирует изменения и добавления стандартов, позволяет в самых первых рядах поиграться с функциональностью из драфтов следующего стандарта C++.
    Clang обладает обширной системой статической и динамической проверки кода: богатый статический анализ, возможность подключения санитайзеров, поддержка C++ Core Guidelines, очень качественные отчеты об ошибках трансляции, хорошая скорость трансляции.
    Это все ставит clang в предпочтение перед GCC на третьих для GCC платформах.

    О полной совместимости между трансляторами.
    Полная совместимость между трансляторами есть. Иначе я бы не мог делать то, что я делаю. А дело мое заключается в создании полностью кроссплатформенного кода, который однозначно собирается на всех целевых платформах и на всех них выполняется так же однозначно.
    Полная совместимость между трансляторами заключается в строгом соответствии кода выбранному стандарту C++. Всё, точка. На этом к трансляторам требования заканчиваются.
    Только тут есть небольшая проблема. Каждый транслятор по-своему поддерживает стандарт и по-своему реализует неоговоренные стандартом механики. Каждый транслятор имеет свои ошибки трансляции. И вскрывается это все именно в процессе работы над кроссплатформенным кодом.

    Я в своей работе видел многое. Я видел как при смене GCC на clang люди хватались за голову и отказывались от последнего просто потому что он нашел горы нарушений стандарта, которые молча принимал GCC. Я видел как группа из 5 человек 3 месяца рефакторила код при переходе с MSVS2015 на MSVS2017 (т.е. просто при смене версии транслятора) просто потому что разработчики из рук вон плохо знают используемый ими стандарт C++.
    Я видел ошибки в clang, приводящие к неверной генерации кода. Я видел ошибки в GCC, не позволяющие использовать его для кроссплатформенной сборки. Я видел ошибки в MSCL, в результате которых последний явно нарушает стандарт, а команда его разработки отказывается это исправлять потому что "иди нафиг".

    И, тем не менее, конкретно у меня есть возможность писать код ровно один раз и собирать его на 5 совершенно разных целевых платформ совершенно разными трансляторами, на которых этот код работает абсолютно равнозначно. Просто потому что я знаю стандарт и то, как этот стандарт поддерживают выбранные мной трансляторы.
    Ответ написан
    3 комментария
  • 2.5D изометрия, как отсортировать спрайты?

    @MarkusD
    все время мелю чепуху :)
    Самым простым, и самым ненадежным, методом сортировки объектов в изометрии является рядная сортировка.
    При такой сортировке вся локация выводится по вертикальным или горизонтальным рядам сверху-вниз.
    Проблемы с такой сортировкой начинаются буквально сразу же, как только объекты сцены перестают занимать полностью весь тайл.
    Например в Red Alert 2 или Tiberian Sun пехота занимает четверть тайла. При вертикальной или горизонтальной рядной сортировке пехота по бокам тайлов будет накрываться тайлами соседних рядов.
    В тех же RA2 и TS есть очень высокие здания. При горизонтальной рядной сортировке такие здания будут накрываться другими тайлами.

    Более сложным методом сортировки будет топологическая сортировка. Чтобы выполнить эту сортировку, сперва надо построить граф затенения, в которым учитывается высота и положение по оси Z объектов на сцене.
    В изометрии оси координат развернуты таким образом, что их проекции образуют между собой равные углы по 120°. Этот момент показывает сравнительно простой механизм определения затенения.
    Любой объект сцены достаточно описать своим AABB и посчитать пересечения многоугольника проекции этого AABB с такими же многоугольниками проекций AABB других объектов сцены дальше от камеры.
    Получается, что в графе затенения записаны все отношения между объектами сцены, кто кого затеняет. И объекты, которые никого не затеняют, являются первыми в списке вывода на экран. Дальше выводятся те объекты, которые затеняют уже выведенные объекты.

    Больше деталей можно найти в самых разных статьях на приведенную тему. Например: [1], [2], [3].
    Ответ написан
    Комментировать
  • Большая ли разница между написанием на UNITY или чистом С++ C#?

    @MarkusD
    все время мелю чепуху :)
    Инструмент всегда выбирается от задачи.

    Первым делом нужно строго сформулировать для себя задачу. Чего ты хочешь добиться?
    Сделать игру? Это не задача. Заработать денег на игре? Это не задача.
    Задачу надо детально формулировать. Например: сделать игру по уже имеющемуся ТЗ в конкретные сроки и с затратами не выше заданного бюджета.

    Исходя из задачи выбирается инструмент.
    Разные инструменты предъявляют разные требования к профессионализму разработчика, бюджету и времени разработки. Разные инструменты влекут разные риски для процесса разработки.
    Выбор инструмента заключается в сведении проф. навыков, бюджета, времени и рисков в одном месте.

    Unity позволяет вести быструю разработку при минимальной квалификации, но влечет риски быстрого изменения системных требований и непредвиденных сопутствующих расходов в местном "магазинчике радостей". Плюс, в определенный момент потребуется заплатить немалые деньги за лицензию.

    Чистый C# требует профессионализма и большого времени на разработку. Профессионализм требуется сразу довольно высокий. Человек должен не только языком владеть, но и быть в состоянии выбрать подходящие сторонние библиотеки для помощи.

    Чистый C++ требует экспертного уровня профессионализма и, буквально, огромного времени на разработку. Незначительного уменьшения времени на разработку можно добиться покупкой сторонних инструментов за довольно большие деньги. Более того, экспертный уровень требуется не только в знании C++, но и в знании сопутствующих разработке игры областей. Математику, звук, графику, сеть и каждую из целевых платформ требуется тоже знать на экспертном уровне. Иначе выбор чистого C++ будет проигрывать выбору чистого C#.
    Современный C# после сборки по своей производительности ничем не отличается от результатов сборки C++, а разработку на C++ вести значительно сложнее.

    Более того, использование чистого инструмента всегда требует от пользователя экспертного уровня знания архитектуры ПО.
    Ответ написан
    Комментировать
  • Является ли хорошим решением разбивать большой класс на несколько .cpp файлов (C++)?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Если дать короткий ответ, то всегда следует задуматься о декомпозиции класса в такой ситуации.

    Подобный твоему класс представляет из себя монолит - довольно распространенный примитив проектирования, попутно именуемый как "God Object". Объект, который может всё и от которого все вокруг зависят.
    Если появляется желание разбить реализацию интерфейса класса на несколько файлов, значит уже есть понимание того, как тематически декомпозировать этот класс и, вероятно, проблема остается только в том, чтобы правильно декомпозировать состояние класса.

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

    Если говорить развернуто.

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

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

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

    Общий шаблон такого разделения выглядит так. Чаще всего разработчики именуют файлы именем класса. Например MyClass.h и MyClass.cpp. Когда нужно тематически разделить определение интерфейса, к имени класса после точки и перед расширением файла добавляется суффикс, говорящий о тематике определения. Например MyClass.serialization.cpp, MyClass.crud.cpp или MyClass.callbacks.cpp.
    Ответ написан
    5 комментариев
  • Как отключить определение функции через шаблоны?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Не так важно просто включить или выключить метод исходя из аргументов шаблона типа, как важно объяснить пользователю типа, почему там метод доступен, а тут - нет.

    Для начала стоит разобраться со SFINAE, ведь именно этот механизм позволяет "выключать" определения в коде при особых условиях.
    Провал вывода шаблона не должен приводить к ошибке трансляции кода. Это - тонкий механизм, который всегда балансирует на грани между трансляцией и ошибкой. Когда нужно выключить определение функции, для нее всегда нужно предоставить альтернативу при иных условиях.
    В этой связи очень важно разделять вывод шаблона самой матрицы и вывод шаблонов методов матрицы.
    Проваливаться должны именно попытки вывода шаблонов методов матрицы. В самом буквальном смысле, метод будет выключаться для неквадратной матрицы потому что его не удалось вывести из шаблона.

    И вот как это должно выглядеть в идеале.
    template< size_t ROWS, size_t COLUMNS >
    struct Matrix final
    {
    	template< size_t R = ROWS, size_t C = COLUMNS >
    	inline std::enable_if_t<R == C, int> GetDeterminant() const
    	{
    		return 0;
    	}
    };


    Важно гарантировать зависимость выражения SFINAE от параметров шаблона метода. Поэтому объявление шаблона выглядит именно так, а не как-то еще.

    Теперь, что будет если GetDeterminant не удалось вывести из шаблона? Будет ошибка трансляции, говорящая о том, что метод не найден. Это ничего не говорит пользователю. Это просто инструктирует транслятор остановить трансляцию. Пользователь, особенно если он не искушен знаниями о SFINAE, не сможет понять причину ошибки трансляции. Такая ситуация создаст риск излишней траты времени на дознание причин ошибки.
    Под конец выяснится что матрица просто не квадратная.
    Пользователю нужно объяснить причину отсутствия метода.

    Есть простой способ сделать это.
    template< size_t ROWS, size_t COLUMNS >
    struct Matrix final
    {
    	template< size_t R = ROWS, size_t C = COLUMNS >
    	inline std::enable_if_t<R != C, int> GetDeterminant() const = delete;
    
    	template< size_t R = ROWS, size_t C = COLUMNS >
    	inline std::enable_if_t<R == C, int> GetDeterminant() const
    	{
    		return 0;
    	}
    };


    Альтернативный путь вывода всегда должен быть. Его отсутствие или приведет к ошибке трансляции, или усложнит понимание для пользователя. В данном случае явно удаленный метод должен подтолкнуть пользователя к верной причине. Сообщение об ошибке будет говорить о том, что пользователь пытается использовать удаленный GetDeterminant.

    Но есть способ вообще уйти от всех этих усложнений и крайне доходчиво донести до пользователя суть его ошибки.
    Дело в том, что методы выведенного из шаблона типа выводятся по мере их использования. Если никто не брал детерминант матрицы, то и метод взятия детерминанта для нее выведен не будет.
    А если матрица не является квадратной, сама попытка вывода метода взятия детерминанта должна быть пресечена.

    И делается это крайне просто.
    template< size_t ROWS, size_t COLUMNS >
    struct Matrix final
    {
    	inline int GetDeterminant() const
    	{
    		static_assert( ROWS == COLUMNS, "Matrix should be square to calculate the determinant." );
    		return 0;
    	}
    };


    SFINAE в решении вопроса вообще не нужен. Он только больше усложнит конструкцию и будет запутывать.
    Достаточно простого статического утверждения с максимально детальным пояснением для пользователя.
    Для всех квадратных матриц метод детерминанта будет выведен без проблем. Для любой неквадратной матрицы вывод метода детерминанта провалится на проверке статического утверждения, а пользователь получит максимально конкретную причину провала.
    Ответ написан
    Комментировать
  • Как правильно освобождать память?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Давай рассмотрим, для начала, строку под комментарием.

    ArrayArray[0] = *new Array<int>{10};
    Что тут происходит.
    ArrayArray[0] вернет ссылку на Array<int>.
    *new Array<int>{10} выделяет память в куче под Array<int>, вызывает инициализатор Array<int>::Array(int length), после чего делает разыменование получившегося указателя на Array<int>.
    После этого для подготовленной ссылки на Array<int> будет выполнен оператор копирования по умолчанию, функциональность которого просто скопирует поля из объекта справа в объект слева от присвоения.
    После этого результат new Array<int>{10} становится утекшим, т.к. указатель на него сразу становится потерян и освободить занимаемую им память становится невозможно.
    Именно поэтому с этой строчкой у тебя "всё отрабатывает нормально". И нет, всё у тебя не отрабатывает нормально потому что память утекла.

    Давай рассмотрим следующую строку.

    ArrayArray[0] = Array<int>{10};
    Что тут происходит.
    ArrayArray[0] вернет ссылку на Array<int>.
    Array<int>{10} инициализирует безымянный локальный объект на стеке, используя инициализатор Array<int>::Array(int length).
    После этого выполняется уже оператор перемещения по умолчанию, суть которого снова сводится к копированию полей из объекта справа в объект слева. После этого оба объекта ссылаются на одну и ту же выделенную память через поле T *m_data.
    Далее безымянный локальный объект уничтожается, освобождая недавно выделенную память. А ArrayArray[0] в этот момент начинает ссылаться на освобожденную память.

    Double deletion происходит тогда, когда ArrayArray в конце работы программы пытается удалить уже освобожденную память в ArrayArray[0].

    В твоем коде на лицо нарушение инварианта типа.
    Что нужно сделать.
    Тебе должно уже стать понятно что проблема в твоем коде связана с поведением операторов копирования и перемещения по умолчанию. Но проблема у тебя, на самом деле, значительно шире. Потому что завтра ты ведь точно захочешь еще и инициализацию копией провести или вернуть объект по значению из функции. В этом случае тебя тоже ждут проблемы.
    Решением твоих проблем будет соблюдение правила 3/5/0.
    Тебе нужно полноценно описать поведение объектов при копировании, перемещении, а так же при инициализации копией и инициализацией через перемещение.
    Ответ написан
    2 комментария
  • Внесение данных в std::vector< GLfloat >?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Вектор перед работой с его индексами необходимо привести к требуемому размеру.
    Делается это с помощью метода resize[?].
    После этого можно обращаться к значению по индексу напрямую.

    Если размер вектора уже определен и необходимо именно вставить данные по индексу, то воспользоваться можно методом insert[?].
    Однако, первым параметром метод требует не индекс, а итератор внутри вектора, куда требуется выполнить вставку. Этот итератор можно получить через смещение итератора начала вектора на требуемый индекс.
    vertexBuffer.insert( vertexBuffer.begin() + 1, x );

    При этом важно контролировать чтобы индекс вставки не выходил за пределы размера вектора.
    Забывать не стоит и о том, что при вставке велика вероятность реаллокации памяти под элементы вектора, в следствии чего уже все итераторы и ссылки на элементы вектора будут инвалидированы.
    Ответ написан
    1 комментарий
  • Можно ли объявить переменную-член класса с помощью метапрограммирования?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Соберем немного конкретики из комментариев.

    Отсюда:
    Только хотелось бы без специализации, чтобы код не дублировать

    Единственным дублированием кода тут будет только заголовок частичной специализации шаблона. В силах писателя определить общий между специализациями код и вынести его в родительский для всех специализаций класс.
    А с подобным дублированием справляется шаблон std::enable_if[?]. Часто его используют для выбора поведения шаблона исходя из черт аргумента шаблона.

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

    Отсюда:
    соответственно во всех методах, где этот член используется можно будет проверку if constexpr (someCondition)

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

    За чистотой кода нужно следить. Подобные вещи, как вот такие блоки if constexpr, нужно убирать в функции. А где эти функции лучше расположить? Верно - прямо там, где для их работы определены данные. Поэтому на самом деле if constexpr не нужен. Нужно определить набор функций с поведением там где оно возможно. А где нет - определить заглушки чтобы клиентский, относительно вариативного поведения, код не нуждался в проверках и мог просто обращаться к вариативному поведению так, как будто оно не вариативно и есть всегда.
    Опять же, тут хорошо подходит пример с DebugName[?], в котором такие заглушки реализованы.
    Ответ написан
    1 комментарий
  • Assignment operator VS Destructor + Placement new, где аргумент placement new - prvalue?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    К современному коду предъявляется немало требований. В частности, код должен отвечать определенным критериям качества чтобы иметь шансы на добавление в основной репозиторий в качестве решения задачи.
    Среди таких критериев качества нередко бывают: соответствие общему стилю кода, соответствие общему стилю форматирования, наличие документации или самодокументируемость, а так же - элементарная понятность для читателя.

    Читатель современного кода не должен задаваться вопросами: что тут делает ядовитый газ, зачем швабры и почему вентилятор такой большой [?].
    Читателю все должно быть понятно практически сразу, у него должно возникать как можно меньше вопросов.

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

    *reinterpret_cast<T*>( &buffer[ sizeof( T ) * head ] ) = T{ std::forward<Args>( args )... };

    Да, тут присутствует Value Initialization и оператор перемещения. Однако, оптимизация этого кода сведет все к довольно короткому и как можно более быстрому коду. Поэтому не стоит беспокоиться об этом на текущем этапе. Лучше беспокоиться о понятности кода для читателя.
    Столь же понятным будет и такой код:
    auto T vicar{ std::forward<Args>( args )... };
    std::swap( *reinterpret_cast<T*>( &buffer[ sizeof( T ) * head ] ), vicar );

    Он не будет вызывать много вопросов, разве только вопрос относительно использования std::swap, но только от людей со слабой привычкой пользоваться STL.

    Однако, такой код может привести к ошибке трансляции в том случае, если T является неперемещаемым. В твоем ЦБ должны присутствовать проверки на перемещаемость, копируемость и возможность размена состояний.
    Если T можно копировать, но не перемещать, использовать стоит оператор копирования.
    И только если ни копировать, ни перемещать T нельзя, следует пользоваться деструктором и размещающим конструктором.

    Я бы рекомендовал все три действия оформить в виде перегрузок шаблона функции со SFINAE, чтобы для любого T включалась только одна конкретная перегрузка шаблона.
    Ответ написан
    6 комментариев
  • Как загрузить картинку в OpenTK c# с прозрачным фоном, при чём сам фон и так прозрачный?

    @MarkusD
    все время мелю чепуху :)
    BitmapData data = bitmap2.LockBits(new System.Drawing.Rectangle(x, y, weight, height),
        ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppRgb);

    Тут получается память изображения. Формат пикселя в data будет XRGB с шириной 32 бита.
    Внимание здесь следует обратить на то, что формат указан как RGB32. Это значит что alpha-канал в формате никак не представлен и, скорее всего, у каждого пикселя будет нулевым т.к. место под него в формате заявлено.

    Далее.
    GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
        OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, data.Scan0);

    PixelInternalFormat.Rgba означает что в памяти GPU текстура будет представлена в формате RGBA с каналами float с нормализацией.
    OpenTK.Graphics.OpenGL.PixelFormat.Rgba означает что формат data.Scan0 нужно воспринимать как RGBA, а PixelType.UnsignedByte означает что каналы в data.Scan0 нужно воспринимать как unsigned byte.

    Тут на лицо несовпадение формата считанного из файла изображения и формата создаваемой текстуры. И если размеры и число каналов между форматами совпадают, то трактовка самих каналов - нет. XRGB != RGBA.
    Все нужно привести к единому формату. Например, из файла читать формат Format32bppArgb. Но в этом случае есть вероятность ошибиться с порядком каналов между Format32bppArgb и OpenTK.Graphics.OpenGL.PixelFormat.Rgba.
    Еще, как вариант, можно инициализировать незадействованный канал в исходных данных.

    Одним словом, после чтения из файла, данные будущей текстуры нужно подогнать под желаемый формат текстуры.
    Ответ написан
  • Как разрабатывать игру на c++ под Android?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Самым простым решением для разработки именно игры будет взять Cocos2d-x, Defold или Godot.
    Это уже готовые решения для разработки игр на различные платформы, в том числе и на Android.
    Все движки предоставляют инструменты именно для С++, в отличие от SDL.

    SDL выполнен на языке C и не является подходящим в контексте разработки на C++.

    Если есть желание заняться именно разработкой движка, а не игры, то вместо SDL стоит взять SFML, который выполнен уже на C++ и является довольно хорошим фреймворком. На базе SFML можно довольно быстро сделать небольшой движок для небольшой игры.
    Ответ написан
    2 комментария
  • Какое связывание у namespace, определённёго в области видимости файла .cpp?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Файлы с расширением .cpp обычно являются точками сборки модулей трансляции [?].
    Характеристики связывания имеют свой эффект только между модулями трансляции. Компоновщик занимается связыванием кода и работает с результатами обработки именно модулей трансляции - объектными файлами.
    Поэтому, если в одном .cpp подключить другой .cpp (исключенный из сборки иными способами), то все элементы с внутренним связыванием любого из этих .cpp будут доступны в них обоих.
    Это будет справедливо и для файлов с расширением .h. Файлы .cpp обычно включают .h и все вместе своим кодом формируют модуль трансляции, в котором доступны все элементы с внутренним связыванием. Даже в коде файлов .h.
    Это важно учитывать чтобы не совершать некоторых ошибок.

    По существу вопроса. Все нестатические (без спецификатора static) элементы именованных пространств имен по умолчанию имеют внешнее связывание [?]. Глобальное пространство является тоже именованным (и доступно через :: без имени слева) и ровно так же наделяет все свои нестатические элементы характеристикой внешнего связывания.

    В противоположность этому, абсолютно все элементы анонимных пространств имен (даже элементы вложенных именованных пространств) имеют характеристику внутреннего связывания [?].
    Довольно распространенной ошибкой является определение анонимных пространств и статических элементов пространств в заголовках, после чего сразу множество модулей трансляции пополняются кодом с внутренним связыванием. Это приводит к разбуханию бинарного кода и усложнению сборки.

    Отдельно хотелось бы обозначить inline.
    Спецификатор inline[?] дает пометку слабого внешнего связывания для любой сущности. Что константа или глобальная переменная, что функция или метод (даже статический), помечаются как сущности с внешним связыванием, которое не нарушает ODR в случае если все определения цели связывания во всех модулях трансляции являются полностью одинаковыми. Если хоть одно определение цели связывания отличается - будет нарушение ODR.
    Компоновщик подберет самое первое определение и встроит его в бинарный код. После этого компоновщик будет только проверять другие встреченные определения на предмет соответствия первому, а линковку кода будет производить только относительно первого встреченного определения.
    Поэтому, если в любом именованном пространстве, в нескольких .cpp определить inline функции с одним именем и одной сигнатурой, но разными телами, то проблем со сборкой будет не избежать.

    Все это можно более детально изучить в статье на хабре.
    Ответ написан
    Комментировать
  • В разных IDE код выдаёт разный ответ, как так?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Это тот самый случай, когда с виду простой код заставляет разобраться во множестве тонкостей языка.
    Для лучшего понимания проходящих в коде процессов сперва требуется внимательно присмотреться к стандарту языка.

    Что стандарт говорит нам о перегрузке операторов?
    A declaration whose declarator-id is an operator-function-id shall declare a function or function template or an explicit instantiation or specialization of a function template. A function so declared is an operator function.

    cout << a.get() << b.get();
    Данный код маскирует два вызова одной функции - std::ostream& operator << ( std::ostream&, int ).

    Тут очень важно уточнить форму перегрузки оператора, т.к. результат поведения этого кода сильно зависит от применяемой формы перегрузки.
    Относительно стандартной формы перегрузки operator << () стандарт говорит что это перегрузка в форме внешней функции.

    Значит приведенный код можно записать как:
    operator<<( operator<<( cout, a.get() ), b.get() );


    И именно с этого момента начинается самое интересное.

    Что стандарт говорит нам о вызове функций? А говорит он совсем разные вещи.
    C++14 [expr.call#5.2.2.8] заявляет, что:
    The evaluations of the postfix expression and of the arguments are all unsequenced relative to one another. All side effects of argument evaluations are sequenced before the function is entered (see 1.9).

    C++17 [expr.call#8.2.2.5] утверждает, что:
    If an operator function is invoked using operator notation, argument evaluation is sequenced as specified for the built-in operator; see 16.3.1.2.

    В результате, если транслировать данный код как код 14-го (или старших) стандарта, поведение у этого кода будет одно. Если же код транслировать как код 17-го (и моложе) стандарта, его поведение будет будет уже другим.

    А что же там с вероятным неопределенным поведением? Ведь неупорядоченная модификация состояния является UB. И, вроде как, cout << a.get() << b.get(); можно упростить до cout << ++i << ++i;, что уже более явно должно показывать наличие UB.
    UB в этом коде нет. И вот почему.

    Для определения порядка вычисления участков выражения следует руководствоваться правилами упорядочивания выражений.
    Среди прочих правил там записаны важные для нас сейчас. Я приведу цитаты.
    2) The value computations (but not the side-effects) of the operands to any operator are sequenced before the value computation of the result of the operator (but not its side-effects).

    3) When calling a function (whether or not the function is inline, and whether or not explicit function call syntax is used), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function.

    5) The side effect of the built-in pre-increment and pre-decrement operators is sequenced before its value computation (implicit rule due to definition as compound assignment)


    16) Every overloaded operator obeys the sequencing rules of the built-in operator it overloads when called using operator notation. (since C++17)

    19) In a shift operator expression E1<<E2 and E1>>E2, every value computation and side-effect of E1 is sequenced before every value computation and side effect of E2. (since C++17)


    До C++17 порядок вычисления операндов cout << a.get() << b.get(); не определен, но поведение этого кода определено. Поэтому при трансляции по стандарту C++14 этот код может выдать или 12, или 21. Но не 11.
    Начиная с C++17 порядок вычисления операндов строго определен и является интуитивным, а результат выполнения cout << a.get() << b.get(); всегда однозначен. При трансляции этого кода по стандарту C++17 (и дальше) в консоль будет выведено всегда и только 12.
    До C++11 поведение кода cout << a.get() << b.get(); не определено.

    Сегодня мы уже не задумываемся о жизни до стандарта C++11, поэтому я не скажу что в общем смысле в этом коде присутствует UB. Я скажу что UB тут нет. Но тем не менее, я бы рекомендовал избегать присутствия подобного кода в проектах даже если используется стандарт C++17 и дальше.
    Ответ написан
    Комментировать
  • В чем смысл определения const int &ref=1;?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для начала стоит немного пробежаться по категориям выражений в современном C++.
    Из статьи будет видно что 1 имеет категорию prvalue. 1 является литералом и не является строковым литералом. Это - литерал с типом int по умолчанию.

    Если снова обратиться к статье и посмотреть свойства prvalue, то будет видно, в частности, что prvalue:
    • имеют всегда полный тип;
    • не имеют размещения, а следовательно не имеют адреса в памяти;
    • могут использоваться для инициализации cvq-lvalue-ref и rvalue-ref.

    Последний пункт говорит о том, что код const int& ref = 1; или int&& ref = 1; является полностью стандартным.
    ref в этом случае будет ссылаться на переданный литерал и отсутствие размещения литерала этому не помеха.

    Смысл подобных выражений в том чтобы позволить написать такой код.
    void foo( const int& ref );
    
    void bar()
    {
      foo( 1 );
    }


    Смыл конкретно кода const int &ref=1; можно найти в том, чтобы не писать магические константы в коде. ref - очень плохое имя. Но голая 1 в коде еще хуже.
    Инженер ПО свой код пишет не для транслятора, а для своих сотрудников. Транслятор разберется в любом коде и по любому синтаксически верному коду всегда произведет работающий исполняемый код. Но другие люди в коде смогут разобраться только тогда, когда этот код написан доступным для понимания образом.

    Чтобы код было легче понять, его документируют. Документация бывает разная. Это могут быть комментарии, это может быть отдельный документ. Но лучше всего код понимается тогда, когда он "самодоукментируется" [1], [2], т.е. когда сам код однозначно поясняет цели своего существования и принципы своей работы.
    Чтобы код сам себя пояснял, имена функций, переменных и констант, имена типов и прочих рукописных конструкций должны как можно более ясно отражать цели своего существования. В частности, чтобы код не пестрил безликими магическими числами [?], эти числа принято обличать в т.н. поясняющие константы.
    Такую константу можно определить как cvq-lvalue-ref, назначить понятное в контексте кода имя и присвоить ей требуемый литерал.
    Ответ написан
    Комментировать
  • Что значит ссылка без типа?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    const &y = x;
    Это не ссылка без типа, а синтаксически неверный код, который не пройдет трансляцию за пределами GCC.

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

    В C++ нет типа по умолчанию, в отличие от С, где типом по умолчанию является int. Если в C код const y = x является синтаксически верным и подразумевает const int y = x, то в C++ этот же код является уже синтаксически неверным и не пройдет трансляцию.
    GCC в твоем коде отходит от стандарта C++ в пользу поведения как в C.
    Ответ написан
    2 комментария
  • Почему сплайны OpenGL прорисовываются не на всех компьютерах?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Дело в том, что функция glMap1[?] имеет довольно узкий коридор поддержки. Она была введена в OpenGL 1.0 и удалена из поддержки в OpenGL 3.2 Core Profile.

    Т.к. ты пользуешься wglCreateContext, система сама решает какой контекст ей для тебя создавать. Это может быть и контекст с версией 4.6, в котором уже нет поддержки функции glMap1f.
    Тебе стоит более точно указывать версию создаваемого контекста. Это можно сделать с помощью расширения WGL_ARB_create_context. Функция wglCreateContextAttribsARB позволяет задавать атрибуты для создаваемого контекста, среди которых ты можешь обозначить и требуемую версию.

    В качестве примера использования этого расширения можно взять такой код.
    Код примера
    // Sample code showing how to create a modern OpenGL window and rendering context on Win32.
    
    #include <windows.h>
    #include <gl/gl.h>
    #include <stdbool.h>
    
    typedef HGLRC WINAPI wglCreateContextAttribsARB_type(HDC hdc, HGLRC hShareContext,
            const int *attribList);
    wglCreateContextAttribsARB_type *wglCreateContextAttribsARB;
    
    // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt for all values
    #define WGL_CONTEXT_MAJOR_VERSION_ARB             0x2091
    #define WGL_CONTEXT_MINOR_VERSION_ARB             0x2092
    #define WGL_CONTEXT_PROFILE_MASK_ARB              0x9126
    
    #define WGL_CONTEXT_CORE_PROFILE_BIT_ARB          0x00000001
    
    typedef BOOL WINAPI wglChoosePixelFormatARB_type(HDC hdc, const int *piAttribIList,
            const FLOAT *pfAttribFList, UINT nMaxFormats, int *piFormats, UINT *nNumFormats);
    wglChoosePixelFormatARB_type *wglChoosePixelFormatARB;
    
    // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt for all values
    #define WGL_DRAW_TO_WINDOW_ARB                    0x2001
    #define WGL_ACCELERATION_ARB                      0x2003
    #define WGL_SUPPORT_OPENGL_ARB                    0x2010
    #define WGL_DOUBLE_BUFFER_ARB                     0x2011
    #define WGL_PIXEL_TYPE_ARB                        0x2013
    #define WGL_COLOR_BITS_ARB                        0x2014
    #define WGL_DEPTH_BITS_ARB                        0x2022
    #define WGL_STENCIL_BITS_ARB                      0x2023
    
    #define WGL_FULL_ACCELERATION_ARB                 0x2027
    #define WGL_TYPE_RGBA_ARB                         0x202B
    
    static void
    fatal_error(char *msg)
    {
        MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONEXCLAMATION);
        exit(EXIT_FAILURE);
    }
    
    static void
    init_opengl_extensions(void)
    {
        // Before we can load extensions, we need a dummy OpenGL context, created using a dummy window.
        // We use a dummy window because you can only set the pixel format for a window once. For the
        // real window, we want to use wglChoosePixelFormatARB (so we can potentially specify options
        // that aren't available in PIXELFORMATDESCRIPTOR), but we can't load and use that before we
        // have a context.
        WNDCLASSA window_class = {
            .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
            .lpfnWndProc = DefWindowProcA,
            .hInstance = GetModuleHandle(0),
            .lpszClassName = "Dummy_WGL_djuasiodwa",
        };
    
        if (!RegisterClassA(&window_class)) {
            fatal_error("Failed to register dummy OpenGL window.");
        }
    
        HWND dummy_window = CreateWindowExA(
            0,
            window_class.lpszClassName,
            "Dummy OpenGL Window",
            0,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            0,
            0,
            window_class.hInstance,
            0);
    
        if (!dummy_window) {
            fatal_error("Failed to create dummy OpenGL window.");
        }
    
        HDC dummy_dc = GetDC(dummy_window);
    
        PIXELFORMATDESCRIPTOR pfd = {
            .nSize = sizeof(pfd),
            .nVersion = 1,
            .iPixelType = PFD_TYPE_RGBA,
            .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
            .cColorBits = 32,
            .cAlphaBits = 8,
            .iLayerType = PFD_MAIN_PLANE,
            .cDepthBits = 24,
            .cStencilBits = 8,
        };
    
        int pixel_format = ChoosePixelFormat(dummy_dc, &pfd);
        if (!pixel_format) {
            fatal_error("Failed to find a suitable pixel format.");
        }
        if (!SetPixelFormat(dummy_dc, pixel_format, &pfd)) {
            fatal_error("Failed to set the pixel format.");
        }
    
        HGLRC dummy_context = wglCreateContext(dummy_dc);
        if (!dummy_context) {
            fatal_error("Failed to create a dummy OpenGL rendering context.");
        }
    
        if (!wglMakeCurrent(dummy_dc, dummy_context)) {
            fatal_error("Failed to activate dummy OpenGL rendering context.");
        }
    
        wglCreateContextAttribsARB = (wglCreateContextAttribsARB_type*)wglGetProcAddress(
            "wglCreateContextAttribsARB");
        wglChoosePixelFormatARB = (wglChoosePixelFormatARB_type*)wglGetProcAddress(
            "wglChoosePixelFormatARB");
    
        wglMakeCurrent(dummy_dc, 0);
        wglDeleteContext(dummy_context);
        ReleaseDC(dummy_window, dummy_dc);
        DestroyWindow(dummy_window);
    }
    
    static HGLRC
    init_opengl(HDC real_dc)
    {
        init_opengl_extensions();
    
        // Now we can choose a pixel format the modern way, using wglChoosePixelFormatARB.
        int pixel_format_attribs[] = {
            WGL_DRAW_TO_WINDOW_ARB,     GL_TRUE,
            WGL_SUPPORT_OPENGL_ARB,     GL_TRUE,
            WGL_DOUBLE_BUFFER_ARB,      GL_TRUE,
            WGL_ACCELERATION_ARB,       WGL_FULL_ACCELERATION_ARB,
            WGL_PIXEL_TYPE_ARB,         WGL_TYPE_RGBA_ARB,
            WGL_COLOR_BITS_ARB,         32,
            WGL_DEPTH_BITS_ARB,         24,
            WGL_STENCIL_BITS_ARB,       8,
            0
        };
    
        int pixel_format;
        UINT num_formats;
        wglChoosePixelFormatARB(real_dc, pixel_format_attribs, 0, 1, &pixel_format, &num_formats);
        if (!num_formats) {
            fatal_error("Failed to set the OpenGL 3.3 pixel format.");
        }
    
        PIXELFORMATDESCRIPTOR pfd;
        DescribePixelFormat(real_dc, pixel_format, sizeof(pfd), &pfd);
        if (!SetPixelFormat(real_dc, pixel_format, &pfd)) {
            fatal_error("Failed to set the OpenGL 3.3 pixel format.");
        }
    
        // Specify that we want to create an OpenGL 3.3 core profile context
        int gl33_attribs[] = {
            WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
            WGL_CONTEXT_MINOR_VERSION_ARB, 3,
            WGL_CONTEXT_PROFILE_MASK_ARB,  WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
            0,
        };
    
        HGLRC gl33_context = wglCreateContextAttribsARB(real_dc, 0, gl33_attribs);
        if (!gl33_context) {
            fatal_error("Failed to create OpenGL 3.3 context.");
        }
    
        if (!wglMakeCurrent(real_dc, gl33_context)) {
            fatal_error("Failed to activate OpenGL 3.3 rendering context.");
        }
    
        return gl33_context;
    }
    
    static LRESULT CALLBACK
    window_callback(HWND window, UINT msg, WPARAM wparam, LPARAM lparam)
    {
        LRESULT result = 0;
    
        switch (msg) {
            case WM_CLOSE:
            case WM_DESTROY:
                PostQuitMessage(0);
                break;
            default:
                result = DefWindowProcA(window, msg, wparam, lparam);
                break;
        }
    
        return result;
    }
    
    static HWND
    create_window(HINSTANCE inst)
    {
        WNDCLASSA window_class = {
            .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
            .lpfnWndProc = window_callback,
            .hInstance = inst,
            .hCursor = LoadCursor(0, IDC_ARROW),
            .hbrBackground = 0,
            .lpszClassName = "WGL_fdjhsklf",
        };
    
        if (!RegisterClassA(&window_class)) {
            fatal_error("Failed to register window.");
        }
    
        // Specify a desired width and height, then adjust the rect so the window's client area will be
        // that size.
        RECT rect = {
            .right = 1024,
            .bottom = 576,
        };
        DWORD window_style = WS_OVERLAPPEDWINDOW;
        AdjustWindowRect(&rect, window_style, false);
    
        HWND window = CreateWindowExA(
            0,
            window_class.lpszClassName,
            "OpenGL",
            window_style,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            rect.right - rect.left,
            rect.bottom - rect.top,
            0,
            0,
            inst,
            0);
    
        if (!window) {
            fatal_error("Failed to create window.");
        }
    
        return window;
    }
    
    int WINAPI
    WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd_line, int show)
    {
        HWND window = create_window(inst);
        HDC gldc = GetDC(window);
        HGLRC glrc = init_opengl(gldc);
    
        ShowWindow(window, show);
        UpdateWindow(window);
    
        bool running = true;
        while (running) {
            MSG msg;
            while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
                if (msg.message == WM_QUIT) {
                    running = false;
                } else {
                    TranslateMessage(&msg);
                    DispatchMessageA(&msg);
                }
            }
    
            glClearColor(1.0f, 0.5f, 0.5f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            // Do OpenGL rendering here
    
            SwapBuffers(gldc);
        }
    
        return 0;
    }
    Ответ написан
    Комментировать
  • Выбор игрового движка для C++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Много решений подходит под такие критерии. Смотри, изучай, выбирай.

    Cocos2d-x является одним из самых популярных открытых движков. У него большое сообщество и масса поклонников. Есть документация и все нужное для старта.

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

    Godot Engine не менее популярен и не менее поднят по возможностям. В чем-то Godot даже будет лучше чем Cocos. Сообщество у него тоже большое. Документация тоже присутствует.

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

    Дальше пойдут не такие популярные решения, однако и проходить мимо них тоже не стоит.

    Urho3D является нареченной Open-Source альтернативой Unity. Движок используется многими энтузиастами. По разным уголкам сети раскиданы многочисленные группы обсуждения этого движка. Документация и примеры у него на месте.

    GDevelop - это довольно популярное решение для небольших игр. Документация на месте.

    Panda3D - тоже довольно популярное решение со своим сообществом. Документация имеется.

    Hazel Engine - один разработчик - один движок. Полностью вся разработка изложена в видео на youtube. Пользоваться можно... на свой страх и риск.

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

    GZDoom - современная инкарнация движка DOOM.

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

    CryEngine - от Crytek.
    X-Ray - движок S.T.A.L.K.E.R.
    UE 3 - для коммерческих проектов использовать нельзя.
    Lumberyard - от Amazon. Да-да, тот самый.
    Banshee Engine - он просто существует.
    Diligent Engine - у него есть свое сообщество.
    Atomic Engine - на нем тоже выпускают игры.
    Lumix Engine - тоже что-то может.
    Horde 3D - просто существует и этого уже достаточно.
    Ответ написан
    Комментировать
  • Как программно рассчитать коллайдеры для спрайтов?

    @MarkusD
    все время мелю чепуху :)
    Существует семейство алгоритмов под названием Convex Hulling, позволяющих с требуемой точностью обернуть изображение в примитив.
    Полученный контур примитива уже можно использовать для заполнения коллайдерами, тоже с требуемой точностью.
    Для заполнения примитива коллайдерами может подойти алгоритм из семейства Bin Packing. Они позволяют учитывать перекрытие и неточность заполнения контура.
    В результате, при подборе реализаций и при подстройке критериев ты можешь получить результат, сравнимый с приведенными на изображениях.

    Однако, лично я рекомендовал бы остановиться уже на контуре примитива изображения. Если это все действительно коллайдеры, то проверка одного замкнутого многоугольника будет дешевле проверки потенциально бесконечной коллекции окружностей.
    Ответ написан
    1 комментарий
  • Почему дружественная функция, определённая внутри класса с первым параметром встроенного типа, недоступна вне определения класса?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Согласно стандарту, дружественная функция может быть определена по месту объявления только для нелокального класса и только если имя функции не является квалифицированным.
    Первое ограничение нас мало интересует, а второе - является довольно существенным.

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

    Функция f5 не просто первично объявлена, она и определена по месту объявления дружественности. Ее имя является однозначно неквалифицированным в следствии своего определения.

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

    Все потому что т.н. имена скрытых друзей могут быть найдены только средствами ADL.
    Если коротко, Argument-Dependent Lookup опирается на типы аргументов при вызове функции, пространства их имен и пространства имен, в которых эти типы объявлены.
    ADL не выполняет поиск в пространствах имен в отношении фундаментальных типов. Поэтому код f5(5); буквально обречен на ошибку трансляции.

    Первично объявленная дружественной функция будет исключительно ADL-доступной до момента своего объявления или определения в том же пространстве имен, в котором определен тип друга. В этом случае функция станет доступна для поиска квалифицированных или неквалифицированных имен.
    Для f5 доступен только способ с повторным объявлением, т.к. она уже определена.
    Да только в этом случае f5 окончательно потеряет всякий смысл быть дружественной для A и станет просто сильно спрятанной и сбивающей с толку глобальной функцией.
    Суть дружественности в раскрытии доступа, которым f5 относительно A не пользуется, т.к. среди ее параметров нет ни одного с типом A.

    В результате.
    Чтобы ADL нашел функцию f5, среди ее параметров обязан быть параметр и с типом A.
    Чтобы UNL или QNL смогли найти функцию f5, ее надо дополнительно объявить за пределами типа A в его пространстве имен.
    Ответ написан
    1 комментарий
  • Как создать окно OpenGL в окне полученном от CreateWindowEx (WINAPI)?

    @MarkusD
    все время мелю чепуху :)
    Использование glfw нередко приводит к тому, что обычные для OpenGL вещи средствами glfw сделать невозможно.
    В рамках glfw контекст OpenGL непосредственно объединен с окном операционной системы в типе GLFWwindow[?].
    Разделить их или использовать по отдельности уже не получится т.к. glfw не дает такого интерфейса.
    Так же, в функцию glfwCreateWindow[?] невозможно передать и дескриптор окна операционной системы если хочется создать дочернее окно в своем.

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

    Максимум что можно сделать средствами glfw - это создать несколько разных окон, каждому из которых будет сопоставлен его уникальный контекст OpenGL. Но работать с такими окнами придется только и строго в разных потоках, т.к. привязка контекста OpenGL производится только для конкретного потока, в котором для этого контекста и нужно вызывать функции OpenGL. Иными словами, такой подход может означать большие трудности при реализации.
    Если присмотреться к сигнатуре glfwCreateWindow, то можно заметить последний параметр, который позволяет указать другое окно, ресурсы которого необходимо разделить с новым создаваемым окном. Используя этот параметр можно создать несколько окон с совместным управлением ресурсами, но рисовать в этих окнах все равно можно лишь в разных потоках.

    Для решения задачи более гибкого создания окон и рисования в них средствами OpenGL лучше зайти со стороны WGL[?] и прямой работы с Win32 API. В этом случае можно даже одним контекстом обойтись, привязывая его то к одному окну, то к другому на время отрисовки.

    Но если требуется просто добавить у себя элементы интерфейса в свое текущее окно, то лучше и удобнее будет взять imgui или подобную библиотеку. Для этого от glfw можно не отказываться.
    Ответ написан
    Комментировать