Ответы пользователя по тегу C++
  • Выбор игрового движка для C++?

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

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

    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 Куратор тега 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 комментарий
  • Как объяснить данный фрагмент кода?

    @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 был доработан таким образом, чтобы при указании опции компилятор выдавал один заголовок и один исходник в независимости от набора и содержимого схем формата.
    Ответ написан
    1 комментарий
  • Как сделать так чтобы функция принимала неограниченное число аргументов разного типа?

    @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 Куратор тега 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 скриптового движка.

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

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

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

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

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

    Функции интерполяции у тебя всегда чистые, т.е. зависят только от переданных им аргументов. Для любого типа данных, от скаляра и до матрицы, можно определить функцию интерполяции между двумя значениями. Это все - внешние относительно ECS вещи, а внутри ECS на каждый ***InterpolationComponent у тебя будет своя система, которая делает выборку по целевому компоненту и по компоненту интерполируемости. Таким образом, уже на стадии выборки компонентов у тебя останутся только подходящие сущности, а все остальные - отфильтруются.
    Заметить стоит еще то, что такой подход позволяет одно конкретное значение интерполировать только одной функцией за раз. Если для одного значения требуется несколько одновременных интерполяций, такой подход уже не подходит. С другой стороны, для того чтобы правильно свести несколько таких анимаций для одного значения, требуется значительно более сложная система, чем просто функция интерполяции.
    Ответ написан
    2 комментария
  • Как избавиться от 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 обладает не меньшей свободой и гибкостью, но предлагает больше.
    Ответ написан
  • Как написать аналог 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>()... );
    	}
    }
    Ответ написан
    1 комментарий
  • Инициализация элемента к нулю?

    @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 должны предусмотреть.
    Ответ написан
    1 комментарий
  • Лучшие источники для изучения CPP?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В самую первую очередь - это будет документация языка. Ее очень удобно использовать как справочник. Это - твой самый первый источник информации по любому вопросу.
    isocpp поддерживается создателем языка и содержит море полезной информации.
    C++ Core Guidelines является манифестом пользователя C++. Его знать обязательно. Документ регулярно дополняется.

    More C++ Idioms. Шаблоны проектирования имеют свою собственную многомерную классификацию. Идиомы - это функциональные шаблоны проектирования, применимые, как правило, или для конкретного языка, или для некоторого семейства языков. Эта открытая книга помогает ориентироваться в некотором начальном наборе идиом конкретно для языка C++.
    C++ Patterns - еще один полезный ресурс для изучения применимых к C++ шаблонов проектирования.
    С Fluent C++ ты уже знаком.
    Безусловно, блог создателей PVS-Studio.
    Habr, конечно же.
    Блогов очень много, их можно просто найти по релевантной фразе "C++ blog".

    Помимо этого есть большое количество каналов от разных конференций, доклады на которых всегда помогают понять язык лучше.
    С++Russia,
    C++Now,
    Pacific C++,
    CppCon,
    code::dive,
    Meeting C++.

    Так же будет полезно изучить книги авторов:
    Андрея Александреску,
    Герба Саттера,
    Девида Вандервуда,
    Скотта Мейерса,
    Роберта Мартина.
    Есть и другие очень полезные авторы. Тут у меня, пожалуй, только самый основной список.

    Последим, и самым важным, источником будет текущая рабочая версия стандарта языка, а так же пара лабораторий для практики: Compiler Explorer и C++ Insights.
    Ответ написан
    Комментировать
  • Как вычислить Z для линии?

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

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

    Допустим, у нас есть тип 3D-позиции.
    struct Vector3i final
    {
    	int32_t x;
    	int32_t y;
    	int32_t z;
    };


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

    Пример типа AxisProxy
    using AxisPointer = int32_t Vector3i::*;
    
    class AxisProxy final
    {
    public:
        AxisProxy( AxisPointer major_axis, AxisPointer middle_axis, AxisPointer minor_axis)
            : m_major_axis{ major_axis }
            , m_middle_axis{ middle_axis }
            , m_minor_axis{ minor_axis }
        {}
    
    public:
        inline int32_t& AccessMajorAxis( Vector3i& value ) const               { return value.*m_major_axis; };
        inline int32_t& AccessMiddleAxis( Vector3i& value ) const              { return value.*m_middle_axis; };
        inline int32_t& AccessMinorAxis( Vector3i& value ) const               { return value.*m_minor_axis; };
    
        inline const int32_t& AccessMajorAxis( const Vector3i& value ) const   { return value.*m_major_axis; };
        inline const int32_t& AccessMiddleAxis( const Vector3i& value ) const  { return value.*m_middle_axis; };
        inline const int32_t& AccessMinorAxis( const Vector3i& value ) const   { return value.*m_minor_axis; };
    
    private:
        AxisPointer    m_major_axis    = &Vector3i::x;
        AxisPointer    m_middle_axis   = &Vector3i::y;
        AxisPointer    m_minor_axis    = &Vector3i::z;
    };


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

    Примерное объявление типа LineIterator
    class LineIterator final
    {
    public:
        LineIterator( const Vector3i from, const Vector3i to );
    
    public:
        const Vector3i& operator ++ ();
        const Vector3i operator ++ ( int );
    
        inline const Vector3i& operator * () const     { return m_current_point; };
        inline const Vector3i* operator -> () const    { return &m_current_point; };
    
    private:
        static inline const int32_t GetCorrectionStepAxis( const int32_t value )   { return std::abs( value ) << 1; };
        static inline const int32_t GetShiftStepAxis( const int32_t value )        { return ( value > 0 ) - ( value < 0 ); };
    
        void PerformLineStep();
    
    private:
        Vector3i   m_current_point;            // Current position at line.
        Vector3i   m_correction_step;            // Values to change the point corrections.
        Vector3i   m_shift_step;                // The shift step for current point in each iteration.
        int32_t    m_middle_axis_correction;    // The marker for middle axis correction.
        int32_t    m_minor_axis_correction;    // The marker for minor axis correction.
        AxisProxy  m_axis_proxy;                // Point fields proxy.
    };


    Это - обычный поступательный итератор, каждый шаг которого отображает движение вдоль заданной при конструировании линии. Его операторы будут очень простыми.
    Пример кода операторов итератора
    const Vector3i& LineIterator::operator ++ ()
    {
        PerformLineStep();
        return m_current_point;
    }
    
    const Vector3i LineIterator::operator ++ ( int )
    {
        Vector3i current_point{ m_current_point };
        PerformLineStep();
        return current_point;
    }


    С инициализацией будет уже поинтереснее.
    Пример кода конструктора итератора
    LineIterator::LineIterator( const Vector3i from, const Vector3i to )
        : m_current_point{ from }
    {
        const Vector3i line_delta{ to - from };
    
        m_correction_step  = { GetCorrectionStepAxis( line_delta.x ), GetCorrectionStepAxis( line_delta.y ), GetCorrectionStepAxis( line_delta.z ) };
        m_shift_step       = { GetShiftStepAxis( line_delta.x ), GetShiftStepAxis( line_delta.y ), GetShiftStepAxis( line_delta.z ) };
    
        AxisPointer axis[3] = { &Vector3i::x, &Vector3i::y, &Vector3i::z };
        std::sort(
            std::begin( axis ), std::end( axis ),
            [this]( const AxisPointer left, const AxisPointer right ) -> bool
            {
                return m_correction_step.*left > m_correction_step.*right;
            }
        );
        m_axis_proxy = { axis[0], axis[1], axis[2] };
    
        m_middle_axis_correction   = m_axis_proxy.AccessMiddleAxis( m_correction_step ) - ( m_axis_proxy.AccessMajorAxis( m_correction_step ) >> 1 );
        m_minor_axis_correction    = m_axis_proxy.AccessMinorAxis( m_correction_step ) - ( m_axis_proxy.AccessMajorAxis( m_correction_step ) >> 1 );
    }

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

    Функция выполнения шага вдоль линии будет очень простой за счет работы с прокси-объектом.
    Пример функции шага итератора
    void LineIterator::PerformLineStep()
    {
        if( m_middle_axis_correction > 0 )
        {
            m_middle_axis_correction -= m_axis_proxy.AccessMajorAxis( m_correction_step );
            m_axis_proxy.AccessMiddleAxis( m_current_point ) += m_axis_proxy.AccessMiddleAxis( m_shift_step );
        }
    
        if( m_minor_axis_correction > 0 )
        {
            m_minor_axis_correction -= m_axis_proxy.AccessMajorAxis( m_correction_step );
            m_axis_proxy.AccessMinorAxis( m_current_point ) += m_axis_proxy.AccessMinorAxis( m_shift_step );
        }
    
        m_middle_axis_correction += m_axis_proxy.AccessMiddleAxis( m_correction_step );
        m_minor_axis_correction += m_axis_proxy.AccessMinorAxis( m_correction_step );
        m_axis_proxy.AccessMajorAxis( m_current_point ) += m_axis_proxy.AccessMajorAxis( m_shift_step );
    }


    Если убрать поле AxisProxy::m_middle_axis из кода и удалить весь код, где на него есть ссылки, то весь оставшийся код будет представлять из себя обычный итератор линии для 2D-пространства. В этом случае оси даже сортировать не надо будет, для инициализации прокси можно будет обойтись одним тернарным оператором.
    Ответ написан
    Комментировать
  • Как разрабатывать оконные приложения на С++ в Visual Studio 2019?

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

    Вторым решением будет чуть более сложное: использование C++/CX вместе с UWP / Windows Forms.

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

    И самым сложным решением будет прямое использование WinAPI.

    Помимо всего этого еще можно воспользоваться библиотекой wxWidgets и на ее базе разработать GUI для своего приложения.
    Ответ написан
    1 комментарий