• Как объяснить данный фрагмент кода?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В данном коде демонстрируется типичная ошибка подмены типа (type punning [?]), ведущая к неминуемому UB [?] по стандарту.

    num в данном случае представляет собой шестнадцатеричное число, при выводе оно переводится в десятичное число типа double.


    Вот что стандарт говорит о подмене типа.
    [basic.lval#11]
    If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined:
    (11.1) -- the dynamic type of the object,
    (11.2) -- a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
    (11.3) -- a char, unsigned char, or std​::​byte type.

    Относительно unsigned long long тип double не обладает ни одним из требуемых стандартом свойств. Отсюда прямой вывод что данный код вносит UB.
    Поэтому под большим вопросом остается то, что выводится данным кодом.

    В итоге, правильным примером подмены типа на другой может быть только пример, где используется подмена на разрешенный тип представления объекта: char, unsigned char или std::byte для C++17.
    C++ Core Guidelines в секции C.183 однозначно не советует для подмены использовать union. Этого действительно не стоит делать даже при условии того, что все современные трансляторы понимают такие конструкции так, как подозревает писатель.

    Почему неверно работает, к примеру, такой фрагмент кода:

    Второй блок кода означает приведение значения из типа unsigned long long в тип double. В этом случае производится именно стандартное преобразование типа, а не подмена с целью интерпретации представления памяти num иным образом.
    0xC068C0C000000000 для типа unsigned long long означает 13864543383726325760, при преобразовании этого значения в тип double значение будет преобразовано в 1.3864543383726326e+19. В таком виде оно и будет выведено.

    Относительно первого блока кода, лучшим примером подмены типа для кода из вопроса будет такой:
    unsigned long long num = 0xC068C0C000000000;
    double representation = {};
    memcpy( &representation, &num, sizeof( num ) );
    cout << representation;

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

    Мне не понятно, как работает такая конструкция, т.е зачем передавать именно ссылку на переменную num?

    Иногда требуется произвести приведение типа значения [?], иногда требуется произвести интерпретацию памяти этого значения другим типом. Это и называется подменой типа или алиасингом. Более детальную информацию о подмене типа, и алиасинге вообще, можно узнать из этой заметки.
    Ответ написан
  • Что значит С++17 в вакансиях? Зачем это пишут?

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

    Между тем, понимание конкретного стандарта языка означает не только знание этого стандарта, но и знание всех стандартов до него, и понимание различий между ними всеми. А так же - понимание цены перехода в коде от более старого стандарта к более новому.
    Понимание C++17, прежде всего, означает понимание цены перехода с C++14 в коде с auto и инициализацией списками, понимание цены отказа от std::tie в пользу структурного связывания (не говоря уже о понимании семантики структурного связывания) и понимание рациональности перехода на структурное связывание в конкретном месте кода, понимание последствий использования автоматического выведения аргументов шаблона типа в месте вызова конструктора и возможные последствия применения этого подхода в конкретном участке кода, понимание семантики работы с this в замыкании лямбды и отличия этого поведения в более старых стандартах. И так далее.
    Ответ написан
  • Есть ли пакет nodejs для объединения списка .cpp и .h в два файла main.cpp и main.h?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Тот факт, что для генерации кода в проекте используется Node.js, не может являться требованием к текущей задаче.
    Генерация кода - это одна задача. Подготовка сгенерированного кода к использованию - другая задача.

    Искомая технология называется Source Code Amalgamation [?]. Еще ее называют Single Compilation Unit (SCU) или Unity Build [?].
    Технология широко используется системами распределенной сборки, например FASTBuild [?] или Incredibuild [?].
    Помимо этого, технология позволяет разработчикам библиотек готовить свои библиотеки в виде single-header или single-header/single-source.

    Для решения задачи можно попробовать завязаться на функциональность Buck [?] или FASTBuild с целью не сборки целевого проекта, а только получения SCU этого проекта.
    Далее с этим SCU можно работать как с обычным файлом исходного кода.
    Самым наивным решением SCU будет подключение всех файлов исходного кода в один через обычный #include. Но этот подход нельзя считать рабочим, а лишь тестовым. Однако, например, GoogleTest [>] использует именно такой подход в поставке all-in-one.
    Далеко не все библиотеки пригодны для работы в виде SCU. Например, в свое время LZ4 [>] не была готова к упаковке в SCU, что привело к ошибкам трансляции при ее сборке. Библиотека потребовала определенной чистки и реструктуризации для успешной упаковки в SCU.
    Для генераторов кода было бы проще генерировать сразу SCU по требованию. Например, в свое время Google Flatbuffers [>] умела генерировать только наборы файлов по принципу "Один тип - один заголовок + один исходник", что в некоторых случаях приводило к генерации сотен и тысяч файлов внутрь проектов. По задаче об амальгамировании исходный код компилятора fbc был доработан таким образом, чтобы при указании опции компилятор выдавал один заголовок и один исходник в независимости от набора и содержимого схем формата.
    Ответ написан
  • Как в OpenGL провести разноцветную ломаную линию без градиента?

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

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

    template< typename... TArguments >
    void Foo( const TArguments&... arguments )
    {
    	static_assert( sizeof...( TArguments ) > 0, "This function shall not be called without arguments." );
    }


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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Как показало обсуждение в комментариях, исключение бросается из функции glTexImage2D.
    Подозрение в этом случае может пасть только на то, что размер переданной через data памяти меньше расчетного размера, который определяет функция исходя из параметров width, height, GL_RGBA, GL_UNSIGNED_BYTE.

    Это может означать что переданные в glTexImage2D параметры не соответствуют считанному изображению. А т.к. width и height получены напрямую при чтении изображения, вопросы возникать могут только относительно заявленного формата изображения - GL_RGBA, GL_UNSIGNED_BYTE.

    Собственно, именно так и оказалось на самом деле. Прочитанное изображение имеет другой формат и меньше каналов, а следовательно и меньший размер относительно расчетного.

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

    @MarkusD
    все время мелю чепуху :)
    Коротко о разрушаемости в Noita излагается в презентации разработчиков на GDC.
    Детальное описание разрушаемости в Jelly in the sky от автора игры: [1], [2], [3].

    В Червяках же реализация разрушаемости довольно простая.
    Мат. модель уровня состоит из битовой матрицы (где поднятый бит является заполненным, а снятый - пустым), и набора функций рисования в этой матрице. В этой битовой матрице изначально генерируется уровень и эта битовая матрица модифицируется в процессе игры. Функции рисования являются стандартными - это рисование линии от точки и до точки с заданной шириной, а так же рисование залитой окружности. Само рисование происходит нулевыми битами.
    По своей сути Червяки являются таким замысловатым редактором для рисования.
    Ответ написан
  • Какой код правильнее?

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

    Вопросов к первому варианту нет. Тут сразу и наглядно видно агрегатную инициализацию, т.к. тип Book является агрегатным.

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

    Чистый код не вызывает у читателя вопросов. Чистый код не молит стереть его или срочно отрефакторить. С чистым кодом приятно работать. Чистый код приятно писать. И самое важное: за чистый код автору не стыдно.
    А вот за грязный код, вроде второго варианта, писателю должно быть стыдно, т.к. писатель таким своим кодом ворует время своих сотрудников и у самого себя в будущем через то, что заставляет тратить это время на то чтобы вчитаться и понять написанное.
    Ответ написан
  • Как создать wrapper для шаблонной функции?

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

    template<
        typename T,
        typename = std::enable_if_t<(is_floating_point_v<decay_t<T>> || is_integral_v<decay_t<T>>)>
    >
    T GetRand(T low, T high)
    {
        return is_integral_v<T> ? GetIntegralRand(low, high) : GetFloatingRand(low, high);
    }

    Безотносительно ошибок определения параметров шаблона, основная ошибка в этом коде находится в строчке
    return is_integral_v<T> ? GetIntegralRand(low, high) : GetFloatingRand(low, high);

    При такой записи вывод обоих GetIntegralRand и GetFloatingRand будет произведен вне зависимости от признаков аргумента T. Для T == int будет произведен вывод GetIntegralRand<int> и GetFloatingRand<int>. Это приведет к ошибке трансляции, потому что GetFloatingRand<int> вывести невозможно.

    Как выйти из этой ситуации. Поскольку ты пользуешься inline-константами, а следовательно и C++17, для тебя доступна конструкция if-constexpr [?].
    Ее суть сводится к тому, что трансляцию проходит только истинная, согласно условию, ветка кода условной конструкции.
    Пример
    template< typename T >
    T GetRand( T low, T high )
    {
        if constexpr( std::is_floating_point_v<std::decay_t<T>> )
        {
            return GetFloatingRand( low, high );
        }
        else if constexpr( std::is_integral_v<std::decay_t<T>> )
        {
            return GetIntegralRand( low, high );
        }
        else
        {
            return {};
        }
    }


    Но и это тебе, на самом деле, не нужно, т.к. оба твоих первых шаблона можно объединить одним именем с помощью SFINAE.
    template< typename TValue >
    auto GetRand( const TValue low, const TValue high ) -> std::enable_if_t<std::is_floating_point_v<std::decay_t<TValue>>, const TValue>
    {
        return low + (high - low) * rand() / RAND_MAX;
    }
    
    template< typename TValue >
    auto GetRand( const TValue low, const TValue high ) -> std::enable_if_t<std::is_integral_v<std::decay_t<TValue>>, const TValue>
    {
        return low + rand() % (high - low + 1);
    }

    SFINAE в определении шаблона указывать стоит только тогда, когда шаблон не использует автоматический вывод аргументов. Только в этом случае будут полные гарантии что SFINAE не вывалится в ошибку трансляции.
    Для шаблонов функций такие ограничения непрактичны. Поэтому условие нужно перенести из определения шаблона в TRT-определение результата функции, т.к. в этом случае тип результата функции выводится после вывода типов параметров функции. TRT (Trailing Return Type) [?] в этом месте поможет сделать вывод типа результата более понятным.

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

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

    Вопрос выбора скриптового движка является очень сложным и зависит от множества неочевидных критериев. Ответ на такой вопрос сможет дать только опытный инженер, неоднократно имевший дело с разными скриптовыми движками. Потому что даже между Lua, LuaJIT и Terra разница по функциональности и особенностям интеграции является очень существенной.
    И, тем не менее, я этот вопрос оставлю открытым. На крайний случай всегда можно взять простой и легкий Lua, если глаза разбегаются, а решения нет.

    Как производят интеграцию. Например - так, так, так, так, так или вот так.
    После интеграции скриптового движка в свой проект, функциональность своего проекта можно прокинуть на сторону скриптов используя непосредственно API скриптового движка.

    Одним из критериев выбора скриптового движка является его производительность. Чтобы не занимать специалистов подобной рутиной, когда-то давно уже были проведены замеры версий различных скриптовых движков. Результаты замеров доступны всем желающим.
    Однако, стоит напомнить, что не всегда самое быстрое решение является самым оптимальным.
    Ответ написан
  • Как организовать Tween систему в рамках ECS (entt)?

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

    Почему дизайн с одним компонентом обречен. Каждая система в ECS должна работать с выборкой по компонентам. В EnTT это делается с помощью представления потока компонентов - entt::registry::view.
    Такое представление оптимальным образом организует последовательность сущностей, в которых точно присутствуют обозначенные в представлении компоненты. Если начать проверять наличие у сущности иных компонент, помимо того что это противоречит концепции ECS, это затронет выборку из памяти за пределами отображения и сильно снизит производительность системы. Поэтому системе не стоит работать с компонентами за пределами своей выборки.

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

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

    Функции интерполяции у тебя всегда чистые, т.е. зависят только от переданных им аргументов. Для любого типа данных, от скаляра и до матрицы, можно определить функцию интерполяции между двумя значениями. Это все - внешние относительно ECS вещи, а внутри ECS на каждый ***InterpolationComponent у тебя будет своя система, которая делает выборку по целевому компоненту и по компоненту интерполируемости. Таким образом, уже на стадии выборки компонентов у тебя останутся только подходящие сущности, а все остальные - отфильтруются.
    Заметить стоит еще то, что такой подход позволяет одно конкретное значение интерполировать только одной функцией за раз. Если для одного значения требуется несколько одновременных интерполяций, такой подход уже не подходит. С другой стороны, для того чтобы правильно свести несколько таких анимаций для одного значения, требуется значительно более сложная система, чем просто функция интерполяции.
    Ответ написан
  • Как избавиться от warning C26812: Enum type VkResult is unscoped?

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

    С другой стороны, C26812 [?] относится к разделу Enum.3 из C++ Core Guidelines [?] и напрямую указывает на место определения нестрогого перечисления. К строке VkResult result = оно отношения не имеет.

    Дополнительно. #pragma warning(push, 0) [?] не делает того, что ты от него ожидаешь.
    Предупреждения подавлять стоит таким образом.
    #pragma warning( push )
    #pragma warning( disable: 26812 )
    #include <GLFW/glfw3.h>
    #pragma warning( pop )
    Ответ написан
  • Почему работает const, а не constexpr для char*?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Дело в том, что указанный слева квалификатор const относится к правой половине спецификации типа до первого модификатора.
    const char& - ссылка на константный символ. Квалификатор - const, модификатор - &.
    const char* - указатель на память константного символа. Квалификатор - const, модификатор - *.

    При этом, указанный справа квалификатор const относится ко всей части спецификации типа левее, включая все модификаторы.
    char* const - константный указатель на память символа.
    const char* const - константный указатель на память константного символа.
    char* const * - указатель на память константного указателя на память символа.
    char& const существовать не может, т.к. квалификаторы не применяется к ссылкам. Тут будет ошибка трансляции.

    И при чем же здесь constexpr? Просто constexpr всегда относится только ко всей спецификации типа со всеми модификаторами.
    const char* - указатель на память константного символа.
    constexpr char* - константный указатель времени компиляции на память символа. Тут нет ошибки, память символа тут считается модифицируемой.

    И если объект с типом constexpr char* получит характеристику ODR-used [?], то после трансляции кода это будет уже объект с типом char* const. Вот так.
    В то же время, строковые литералы имеют тип const char[N], т.е. статически определенный массив константных символов. Такой тип можно привести только к типу const char*.

    В результате, чтобы правильно определить константный указатель времени компиляции на память константного символа, нужно тип определить как constexpr const char*.
    И const в этом месте никакого прямого отношения к constexpr не имеет.
    Ответ написан
  • Разработка игр для андроид на языке С++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    C++ не является полноценно "родным" языком для Android. Но для этой платформы можно вести разработку на C++.
    Такие движки, как cocos2d-x [?], Urho 3D [?], Unreal Engine 4 [?], Godot Engine [?] и, буквально, море менее известных проектов, позволяют вести разработку игр с использованием языка C++.

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

    Если использование C++ для тебя является важным, о Unity можно забыть. Это взаимоисключающие инструменты.
    И это не важно. Тот же Urho 3D позиционируется как Open Source Unity. Godot Engine обладает не меньшей свободой и гибкостью, но предлагает больше.
    Ответ написан
  • Какая разница между абстрактной фабрикой и фабричным методом?

    @MarkusD
    все время мелю чепуху :)
    Фабричный метод [?] - это т.н. идиома проектирования, т.е. минимальный конструкционный блок проектирования ПО.
    Еще фабричный метод называют виртуальным конструктором.
    Суть фабричного метода сводится к тому, чтобы позволить создавать экземпляры некоторого интерфейса посредством функции с оговоренной сигнатурой. Это может звучать непонятно, но лишь до тех пор, пока дело не дойдет до практики.

    Давай зачитаем XML [?] в DOM [?].
    Пример
    В составе XML есть документ, заголовок документа, элементы, текст, комментарии и управляющие символы. В рамках DOM все составные части документа будут представлены своими классами, объединенными общим интерфейсом XmlNode.
    В процессе разбора текста XML нам придется создавать экземпляры тех или иных классов исходя из того, прочитали ли мы "<?", или "<!--", или "<![CDATA[", или просто "<".
    Во множестве текущих парсеров XML люди как будто ничего про фабричный метод не знают и, в результате, их разбор сводится к массивным условным конструкциях. Мы же пойдем по пути фабричных методов и для каждого класса создадим фабричный метод CreateXmlElement, который будет возвращать экземпляр интерфейса XmlNode и иметь одинаковый набор параметров, а внутри - будет создавать и возвращать экземпляр соответствующего класса элемента XML.
    После этого мы можем создать простой массив фабричных методов и просто дергать нужный метод CreateXmlElement в нужном месте разбора текста XML, получая экземпляр общего интерфейса XmlNode и передавая ему управление дальнейшим разбором текста чтобы элемент смог себя прочитать. Т.к. XmlNode - это интерфейс, процесс зачитки текста зависит от скрытой за этим интерфейсом реализации конкретного типа элемента.
    В результате, за счет использования лишь одного фабричного метода нам удалось сильно упростить код чтения довольно сложного формата.
    Фабричным методом всегда можно распоряжаться как функцией, передавая его, например, в какую-либо рутину, где требуется создавать экземпляры определенного интерфейса. Рутина может быть одна, но за счет передачи разных фабричных методов ее поведение можно изменять.


    Абстрактная фабрика [?] - это уже шаблон дизайна, т.е. шаблон проектирования стоящий на более высоком уровне относительно фабричного метода. Утвержденный дизайн формирует варианты реализации, в рамках которых могут применяться те или иные идиомы.
    Абстрактная фабрика - это всегда интерфейс, это всегда коллекция, это всегда функциональный объект и это всегда прямая связь с фабричным методом.
    По своей сути тот самый массив фабричных методов из примера с чтением XML уже будет являться определенной реализацией абстрактной фабрики. По индексу массива мы говорим, какой элемент нам нужен и через прямой вызов фабричного метода получаем нужный нам экземпляр.

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

    Пример
    Допустим, в функции чтения XML мы хотим избавиться от массива фабричных методов и перейти на абстрактную фабрику потому что появились требования к памяти, занимаемой элементами DOM. В одном случае нам разрешено пользоваться виртуальной памятью, а в другом - все элементы должны быть уложены в памяти некоторой арены.
    Хорошо, мы заводим интерфейс абстрактной фабрики XmlElementFactory, в котором будет функция Create. Далее мы создаем ArenaXmlElementFactory и RamXmlElementFactory от интерфейса XmlElementFactory, в которых реализуем требуемые правила управления памятью.
    Функция чтения XML перестанет оперировать массивом фабричных методов и начнет принимать абстрактную фабрику элементов по ее интерфейсу. При чтении достаточного участка текста, функция чтения XML просто обратится к функции Create переданной ей абстрактной фабрики и получит все тот же экземпляр интерфейса XmlNode.
    Далее мы можем создать фабрику нужного типа, подготовить ее к работе и передать ее в функцию чтения XML. В результате DOM будет построен, согласно переданной фабрике, или в памяти арены, или в динамической памяти.


    Собственно вот и все отличия. Абстрактная фабрика - это дизайн, а фабричный метод - это идиома.
    Будучи идиомой, фабричный метод практически применим лишь в некоторых языках, т.к. в иных языках он просто не нужен по своей сути. Например, в python или js фабричный метод не применим, т.к. эту функцию выполняет конструктор типа. А вот в C++, например, без фабричного метода абстракцию порождения экземпляра интерфейса реализовать невозможно.
    Абстрактная фабрика, будучи шаблоном дизайна, применима и реализуема везде средствами самого языка или применимых к нему идиом проектирования.
    Ответ написан
  • Как написать аналог std::move?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Допустим мы подумали что вот так должна выглядеть функция перемещения.
    template<class T>
    T&& mymove(T& obj) {
      return obj;
    }


    Данная функция принимает lvalue reference типа T и возвращает rvalue reference того же типа T. Записанный в теле код не соберется потому что lvalue reference не может быть неявно приведена к rvalue reference. Для такого приведения легче всего использовать std::move, но мы ведь именно его и хотим сделать.

    Проще всего приведение сделать через static_cast [?]. Это приведение типа подходит для ручного преобразования lvalue reference в rvalue reference, особенно если с обеих сторон приведения одинаковый тип (смотрим на (3) в документации).
    template<class T>
    T&& mymove( T& obj )
    {
        return static_cast<T&&>( obj );
    }


    Такой код даже начнет работать, но только для выражений категории lvalue. Если в mymove попробовать передать литерал, local temporary или rvalue, трансляция снова нарушится. Это потому что что параметр mymove имеет категорию lvalue reference.

    В С++11 была добавлена техника сокращения ссылок - RCR или Reference Collapsing Rule [?]. Эту технику можно использовать для того чтобы в параметре mymove указать универсальную ссылку. Когда шаблон функции будет инстанцирован, шаблонный параметр T будет ссылаться на действительный тип переданного аргумента.
    template<class T>
    T&& mymove( T&& obj )
    {
        return static_cast<T&&>( obj );
    }


    И этот код начнет работать с другими категориями выражений. Даже с cvq lvalue reference приводя тип результата к cvq lvalue reference. Это - не то поведение, которого стоило бы ожидать от перемещения. Более того, теперь для lvalue reference наш mymove больше не делает приведение к rvalue reference. Кажется, случай с cvq lvalue reference или cvq rvalue reference следует как-то особым образом обрабатывать, а lvalue reference - отдельно.
    Например, можно попробовать просто снять константность типа и получить гарантированный UB в качестве результата.
    Но если внимательно изучить поведение RCR, то можно заметить что механизм работает не только для аргумента функции, но и для типа результата, и даже для используемой в static_cast конструкции типа.
    Наверное стоит попробовать освободить выведенный действительный тип T от ссылочности.
    template<class T>
    std::remove_reference_t<T>&& mymove( T&& obj )
    {
        return static_cast<T&&>( obj );
    }


    Этот код снова перестал транслироваться для lvalue reference и cvq lvalue reference по понятным причинам. Теперь для вывода результата функции выключен RCR и функция всегда будет возвращать rvalue reference или cvq rvalue reference. В это время для static_cast RCR все еще применяется, а значит его нужно выключить и там тоже.
    template<class T>
    std::remove_reference_t<T>&& mymove( T&& obj )
    {
        return static_cast<std::remove_reference_t<T>&&>( obj );
    }


    И теперь функция mymove начнет работать так же, как и функция std::move. Она начнет работать с lvalue reference, переводя их в rvalue reference. Она будет оставлять rvalue reference без изменений, пропуская их на выход. Она будет обрабатывать cvq lvalue reference и cvq rvalue reference, выдавая на выходе cvq rvalue reference и не задавая вопросов о том, зачем автору кода это могло бы понадобиться.
    Ответ написан
  • Variadic template, как перебрать в шаблоне все типы по очереди?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Пусть у нас есть некий аксессор:
    template< typename TComponent >
    TComponent* const QueryComponent();


    Пусть у нас есть, для простоты, некоторый обработчик:
    void PerformWorkflow( A*, B*, C* );
    Тут A, B и C - это типы компонентов.

    Чтобы подружить обработчик с аксессором необходимых ему компонентов, понадобится самый простой Parameter pack expansion.
    template< typename... TComponents >
    void ApplyComponents( void (*workflow)( TComponents*... ) )
    {
    	workflow( QueryComponent<TComponents>()... );
    }

    Здесь можно посмотреть как работает пример этого кода.

    В комментарии я вижу несоответствие типа параметра функции для each и типа результата get. Стоит напомнить что разыменование nullptr, который может вернуть get, приводит к неопределенному поведению [?].
    Поэтому, если распаковку переписать на *QueryComponent<TComponents>()..., т.е. на лету разыменовывать полученные указатели, то перед запуском функции для твоей entity следует убедиться что у нее есть все необходимые компоненты. Такая проверка является необходимой для запуска систем в ECS, т.к. система должна обрабатывать только удовлетворяющие своим фильтрам сущности.

    Для этого уже потребуется использовать Fold expression из C++17.
    template <typename... T>
    void each(typename identity<std::function<void(T& ...)>>::type func)
    {
    	for(auto &entity: m_entities)
    	{
    		if( !( (entity.HasComponent<T>() && ...) ) )
    		{
    			continue;
    		}
    		
    		func( *entity.get<T>()... );
    	}
    }
    Ответ написан
  • Как связать игровое клеточное поле в Entity Component System?

    @MarkusD
    все время мелю чепуху :)
    Вообще, пространство игрового поля не очень хорошо выражать в терминах ECS. Игровое поле - это, как правило, монолитный участок логики, для которого лучше подошел бы подход CBSE(COP). Обычно игровое поле представляется композицией нескольких пространств, в которых работают механики игры. Такие пространства удобнее выражать компонентами из CBSE, а не компонентами из ECS.

    Однако, не смотря на это все, есть хороший способ реализации игрового поля терминами ECS. Особенно если игровое поле дискретно.
    Чтобы начать думать в этом направлении, сперва требуется изучить клеточные автоматы: [1], [2].
    Когда с принципами работы клеточных автоматов станет понятнее, стоит обратить свое внимание на шаблон инверсии контроля. При прямом контроле всем твоим миром руководит квант времени, который спускается с главного цикла до каждой сущности. Такой подход неприемлем на игровом поле в терминах ECS.

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

    И теперь самое интересное. Игровые сущности теперь тоже являются компонентами игрового поля. И за счет все той же инверсии контроля игровые сущности теперь обновляться могут только для возбужденных клеток поля. Если клетка спокойна, стоящий на ней персонаж не получает квант обновления. Это - важное правило, которое требуется соблюдать чтобы не нарушать целостность реализации игрового поля через ECS.
    Любое управление персонажем может только возбуждать клетку игрового поля, где стоит персонаж. Когда персонаж перемещается в другую клетку, между клетками переходит не сам персонаж, а его компоненты. Буквально, сперва делается перемещение данных компонентов персонажа в сущность целевой клетки, а потом происходит удаление компонентов персонажа в текущей клетке.

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

    Через такой подход вполне возможно реализовать что-нибудь вроде Factorio.
    Ответ написан
  • Инициализация элемента к нулю?

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

    В контексте вопроса полезно будет рассмотреть два отдельных вида инициализации: инициализацию по умолчанию и инициализацию значением.

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

    Интереснее будет рассмотреть поведение инициализации значением.
    Форма T() может привести к вызову конструктора с пустым std::initializer_list, к вызову конструктора по умолчанию, а может свестись к инициализации по умолчанию в том случае, если конструктор по умолчанию удален или по своим причинам пропущен в пользовательском типе.
    Каждый, кроме последних двух, пункт поведения инициализации значением оперирует словом class, т.е. имеет отношение только к сложным типам. Последний пункт описывает поведение инициализации для всех неописанных выше случаев как инициализацию нулем.
    И именно инициализация нулем написана в примере вопроса.

    Форма T() хорошо подходит для инициализации локальных временных объектов, а для инициализации именованных объектов лучше подходит форма T object {};.
    Форма T object = T();, на самом деле, выполняет инициализацию копией, где в качестве копии используется локальный временный объект T().

    В результате, конкретно конструкция T() или T{} может привести к вызову конструктора по умолчанию или вызову конструктора от пустого std::initializer_list для сложного типа и инициализацию нулем для простого типа.
    Это - очень полезное поведение в метапрограммировании, где у тебя может быть шаблон типа, в котором требуется по значению разместить объект с типом параметра шаблона. Этот объект требуется инициализировать правильным образом, но ты никак не можешь классифицировать тип, передаваемый параметром шаблона. Благодаря инициализации значением можно не задаваться вопросами природы типа из шаблонного параметра.

    Но увлекаться этим подходом тоже не стоит. Я предпочту вариант std::string string; варианту std::string string = std::string(); потому что для std::string инициализация по умолчанию сделает точно то же самое, будет эффективнее (т.к. во втором случае будет копирование) и будет понятнее.
    Код HRESULT res = HRESULT(); стоит заменить на инициализацию одним из базовых значений. Например - кодом S_OK. Это создаст понимание базового состояния программы в месте определения переменной. Тип HRESULT имеет очень сильную семантику и сразу о многом говорит подготовленному читателю.
    Только HWND window = HWND(); или HWND window{}; на самом деле смотрится к месту, т.к. в WinAPI нет предопределенного начального значения и объект проще провести через стандартную инициализацию значением. Уж этот краевой случай разработчики WinAPI должны предусмотреть.
    Ответ написан
  • Как организовать структуру игры Монополия на ECS?

    @MarkusD
    все время мелю чепуху :)
    Самой главной ошибкой начинающих работать с ECS людей является попытка засунуть в ECS сразу и вообще всё.

    У тебя игра по типу монополии. Что есть в монополии?
    В игре заявлены игроки, дискретное поле, активы и события. Еще могут быть заявлены дополнительные правила.
    Игроки и активы имеют свои эффекты и могут быть деактивированы событиями. Собственно, это и есть сущности игрового ECS-мира.

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

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

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