Ответы пользователя по тегу C++
  • Как начать программировать с использованием DirectX?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    ISBN 978-1-56881-720-0
    Jason Zink, Matt Pettineo, Jack Hoxley: "Practical rendering and computation with Direct3D 11".

    На русском книги нет. Книга легко читается и в оригинале. Уже с первых глав дает понимание современного пайплайна графики, стадий инициализации систем DirectX 11, способов загрузки и представления в памяти требуемых для отрисовки данных. В книге хорошо описан и язык HLSL.
    Книга хорошо подходит для холодного старта в работе с DirectX 11.
    Ответ написан
    Комментировать
  • Что стоит учить с или c++ или c#?

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

    Языки программирования - это инструменты. А инструменты всегда надо выбирать от условий задачи. У тебя не выйдет решать все задачи только каким-то одним языком. Знать во всех тонкостях и уметь использовать строго по назначению Python, Java, C#, C++, Lua, TypeScript и, например, PHP не просто нормально, а важно.
    Бьерн Страуструп один раз говорил о том, что для настоящего инженера важно знать порядка 5 разных языков и разбираться в их особенностях, чтобы считать себя настоящим специалистом.

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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для подобных целей уже давно заведены intptr_t и uintptr_t [?], а еще ptrdiff_t [?].
    Именно эти типы и стоит использовать для прямой работы с адресами.

    Теги C и C++ смешивать не совсем уместно. Это мешает выбору более подходящего варианта ответа.
    В C++, например, если нужно только хранить адрес и позволять с ним только определенные операции, лучше мог бы подойти enum class MemoryAddress : uintptr_t;. Пустое перечисление с достаточной шириной и выравниванием избавит от возможности случайно что-то куда-то прибавить или умножить, да и от неявных преобразований убережет. А перегрузка операторов поможет разрешить только определенные операции.
    Но в C так не получится.
    Ответ написан
    Комментировать
  • C++ CMake Как исправить ошибку?

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

    Файлы сейчас сохранены в какой-то другой кодировке, когда GCC в составе MinGW по умолчанию ожидает кодировку UTF-8.
    Достаточно будет сконвертировать файлы в кодировку UTF-8 и MinGW начнет их переваривать.
    Ответ написан
    Комментировать
  • Что за странная запись в С++?

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

    Насколько видно, базово механизм делится на две ветви: поиск квалифицированных имен и поиск неквалифицированных имен.
    И именно в этот момент речь заходит о т.н. квалификации имени выражения.
    Квалификация имени выражения тем полнее, чем точнее от самого корневого пространства имен (т.е. от ::), через все пространства имен и пространства составных типов, написано имя выражения.

    Имя Process::WaitForExit, хоть и является уже квалифицированным за счет указания пространства типа, в котором метод объявлен, все еще остается недостаточно квалифицированным чтобы считаться полностью квалифицированным.
    Вызов метода по его полной квалификации выглядел бы так:
    process.::base::Process::WaitForExit(&exit_code);

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

    Для чего нужно было писать полную квалификацию конкретно в приведенном коде?
    А кто его знает. Метод base::Process::WaitForExit[?] не является виртуальным чтобы сделать предположение о невиртуальном вызове.
    Просто автору так захотелось, наверное.
    Ответ написан
    2 комментария
  • Ошибка в Visual studio opengl. Как решить?

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

    __imp_glClear и __imp_glDrawArrays - это стандартные функции OpenGL, определены они в библиотеке opengl32.lib, которую и требуется подключить как внешнюю зависимость к твоему проекту.

    Зависимости в проект подключаются через свойства проекта Visual Studio.
    Ответ написан
    5 комментариев
  • Почему в Java изменение интерфейса базового класса посредством модификации сигнатуры разрешено?

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

    cat1 -> sound();
    Тут по пунктам. Сперва UNL определит имя cat1 как переменную в локальном пространстве с типом Cat*.
    Далее UNL же определит что в пространстве класса Cat есть метод sound.
    Далее для определения перегрузки Cat::sound запустится ADL и найдет в пространстве только одну перегрузку - метод без параметров. ADL вернет объявление этой перегрузки, т.к. она удовлетворяет условиям вызова метода.

    Отдельно отметить стоит то, что это именно писатель определил в пространстве класса только одну перегрузку метода sound. Алгоритм ADL довольно строг и не будет искать объявления где-либо еще.
    Именно поэтому при данном определении класса Cat код cat1 -> sound(1); трансляцию никогда не пройдет. Просто потому что в пространстве класса написана только одна перегрузка метода.

    Что делать, когда перегрузки из родительского класса нужны все, но замещать их все в классе дочернем нужды нет?
    Тут на помощь приходит ключевое слово using[?].
    Это слово нужно использовать в контексте делегирования из пространства родительского класса. Определение класса Cat должно быть таким.
    class Cat : public Animal
    {
    public:
    	// Delegate all overloads of `sound`.
    	using Animal::sound;
    	
    	void sound () override
    	{
    		std::cout << "Cat.sound()" << '\n';
    	}
    };


    В этом случае код cat1 -> sound(1); пройдет трансляцию и приведет к вызову void Animal::sound(int i).
    Ответ написан
    2 комментария
  • Почему утверждается, что int32_t имеет ширину точно 32 бита, если он является всего лишь псевдонимом int, который может быть больше 32 бит??

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

    Да, стандарт утверждает что тип int может иметь размер не меньше 16 бит.
    Таблица имеет название "Minimum width", т.е. минимальный размер. Минимальный - это значит что int может иметь размер в 16 бит. А может иметь и 64 бита.

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

    Ошибка в рассуждениях заключается в том, что ты проводишь нить только от int32_t к int и дальше к стандарту. В то время как стандарт определяет и требования к типу int32_t тоже.
    Тип int32_t всегда и для любой модели памяти выбирается таким образом чтобы гарантировать размер в 32 бита.

    То что где-то int32_t является псевдонимом int - это не более чем временное совпадение.
    Ответ написан
    1 комментарий
  • Почему не работает метод clone для класса Test1?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Типом результата Test::clone является std::shared_ptr<Test>.
    Строчка Test1 asd = v[1]->clone(); эквивалентна строчке Test1 asd = std::shared_ptr<Test>{ ... };.
    Оператора или конструктора преобразования из std::shared_ptr<Test> у типа Test1 нет. Трансляцию строчка Test1 asd = v[1]->clone(); не пройдет.

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

    Когда ты заранее знаешь тип, тебе незачем пользоваться клонированием, потому что ты можешь просто скопировать стандартным способом.
    Правильно твоя строчка должна выглядеть так: std::shared_ptr<Test> asd = v[1]->clone();.
    Или так:
    Test1 asd{ *std::static_pointer_cast<Test1>( v[1] ) };
    .
    Ответ написан
    Комментировать
  • Использование шаблона в многофайловом проекте, как реализовано в vector например?

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

    Что это означает на практике. После 4й стадии трансляции формируется модуль трансляции, содержимое которого и компилируется в объектный файл. Код модуля трансляции должен быть исчерпывающим, в нем должны находиться все необходимые для компиляции определения. А определение шаблона относится именно к таким требуемым.
    Значит определение используемого в модуле шаблона должно быть внесено в модуль трансляции. А это значит что или определение должно быть вписано в тот .cpp файл, в котором шаблон инстанцируется, или определение должно быть добавлено через директиву #include.

    В первом случае, когда используемое в .cpp файле определение шаблона вписано в том же .cpp файле, определение шаблона будет достижимо только в данном .cpp файле и больше нигде. В этом случае попытка инстанцировать шаблон в другом модуле трансляции приведет к ошибке трансляции, т.к. там определние шаблона будет уже недостижимо.
    Файлы исходного кода включать через директиву #include - это дурной тон и прямой путь к нарушению ODR.

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

    Широкой практикой является разделение кода шаблонов на несколько типов файлов. В .h файлах обычно делают или объявления шаблонов функций, или определения шаблонов классов. В .hpp/.inl файлах обычно делают определения шаблонов функций и шаблонов методов.
    При этом .hpp/.inl файл очень часто включается в самом низу .h файла с его объявлениями.
    Моя личная рекомендация: использовать в таких случаях расширение .inl (от слова inline), т.к. для .hpp столь же широко закреплено взаимоисключающее с .h значение заголовка C++ кода. И видеть эти два расширения в одном проекте обычно бывает странно.

    Вектор же, например в проекте LLVM, реализован так, что часть определений в нем сделаны по месту объявления, а часть - как внешние определения сразу после определения шаблона вектора. Все это сделано прямо в одном заголовке.
    Ответ написан
    Комментировать
  • Актуальны ли книги Александреску, Майерса и Саттера?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Да, книги Андрея Александреску, Скотта Майерса, Герба Саттера, Николая Джосаттиса, и, например, Девида Вандервуда имеют актуальность и по сей день вне зависимости от года издания или перевода.

    C++ развивается вот уже 38 лет. Новые стандарты сегодня приходят с достойной одобрения частотой, но начиная с C++11 изменения в стандартах до сих пор ничего кардинально не ломают. Даже новые возможности концептуально связаны с опытом прошлых стандартов.
    С другой стороны, трансляторы. Новые стандарты языка не приходят сразу, сперва требуется дождаться их поддержки в современных трансляторах. А это происходит не в одно время и не сразу по выходу нового стандарта. Да и когда появляется версия с поддержкой нового стандарта, эта поддержка не лишена ошибок, опознать которые способен только опытный инженер с экспертизой в новом стандарте.
    Разработка же и вовсе не поспевает за трендами. На собеседованиях я то и дело слышу как где-то кто-то еще только вчера и еще только решил перейти на C++11. В 23-м году.
    Самым широко используемым стандартом сейчас является C++17, большинство функций которого многими компаниями так до сих пор и не используется. Люди до сих пор еще только привыкают к нему.
    Книги представленных авторов в понятной форме передают читателю ценный базовый опыт, который можно применять вообще не привязываясь к стандарту языка. Главное - это не брать в рассмотрение книги до 2011 года.

    Чтобы быть на острие развития языка, нужно не книги читать, а быть сильным энтузиастом и иметь изначально глубокую экспертизу в стандартах языка. Авангард развивается за счет самостоятельных экспериментов и исследования пределов возможностей последних стандартов C++. Не за счет ожидания и чтения книг.
    Ответ написан
  • Почему доступ к элементам vector-а O(1)?

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

    Такое поведение имеет список, односвязный или двусвязный.

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

    Именно поэтому доступ к любому элементу вектора имеет сложность O(1). Это просто смещение на индекс элемента относительно начала блока памяти с помощью адресной арифметики.
    Ответ написан
    Комментировать
  • Вопрос по оформлению кода C++?

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

    1) Как называть переменные:

    Зависит от того, как будут называться функции и константы.
    И вот почему
    Книжки читают быстро, а код - еще быстрее. При беглом чтении всегда нужно уметь разделять переменные, константы и функции. C++ итак сложный, а если все будет написано в одной манере, то код на C++ будет только еще сложнее.
    Моей рекомендацией будет переменные и локальные константы писать в lower_cast_snake_style, а глобальные константы, макроопределения и элементы нестрогих перечислений писать в UPPER_CAST_SNAKE_STYLE.
    Таким образом достигается единообразие. Стиль змейки во всех своих видах сразу отходит под описание данных, создавая акцент для читателя. Таким образом данные будут читаться легче.
    Свои типы, имена элементов строгих перечислений, имена пространств и имена функций с методами, при этом, стоит писать в UpperCamelCase. Почему все эти и только в одном стиле. А потому что они и концептуально связаны, и разделены настолько, что не перемешиваются при чтении.
    Все составные типы формируют свои пространства имен для вложенных объявлений. Поэтому строгое перечисление, структура, класс или пространство имен разумно называть в едином стиле.
    Функции являются точками входа в подпрограмму, их стилистически неверно было бы писать, например, в lowerCamelCase. Первая заглавная буква много значит при чтении, она является акцентом для читателя.


    2) Что лучше присваивать булевым переменным:

    Литералы 0 и 1 имеют тип int. Если тип переменной - bool, то с какой стати справа от типа должны присутствовать значения с типом int?
    Следует использовать только литералы с типом bool: true и false.
    И вот почему
    При написании кода самым важным является не отражение алгоритма или формальное соответствие стандарту, а именно не вызывать вопросов у читателя. Нужно всегда понимать, что у читателя свой контекст, читатель решает свою задачу, здесь у тебя в коде он только для сбора информации. Его не должны сбивать с толку никакие изыски в написанном коде. Когда читатель видит слева тип bool, а справа значение с типом int, у него появляются вопросы, закрадывается подозрение в достоверности прочитанного, он выпадает из своего контекста. Это - очень плохо.


    3) Как лучше называть переменые итераторы во вложенных циклах:

    Абсолютно каждое имя должно отвечать на вопрос: "Зачем ты тут существуешь?"
    Могут ли однобуквенные имена ответить на этот вопрос внятно через всего одну свою букву? Нет.
    Имя - это смысл. Имя - это причина существования. Имя - это цель использования.
    И вот почему
    Чтение кода вынуждает читателя создавать и поддерживать некоторый контекст читаемого кода. Чем сложнее читателю дается поддержка такого контекста, тем менее понятен читаемый код и тем больше времени уйдет на его изучение. Если же в результате читателя выкинет из контекста решаемой им задачи, то это будет совсем плохо и виноват в этом будет именно плохо написанный код.
    Написанное в коде имя создает отметку в контексте для читателя. Чем более это имя понятно и отвечает общему изложению кода, тем легче читателю дается поддержка контекста читаемого кода.
    Существует масса концепций именования, море семантических пар имен, гора ярких и кучи общих имен. Важным остается только одно - переменная должна своим именем говорить о том, что она хранит, а функция - что делает. Тип должен в своем имени раскрывать природу существования своих объектов.
    Имя должно быть обязательно конкретным. Data, Interface, Iterator - это общие имена, которые не несут никакой конкретики. Общие имена допускаются только в абстрактном коде, т.е. в коде интерфейсов, шаблонов, макросов. Между именем вызываемой функции и именем переменной, принимающей результат вызываемой функции должна быть семантическая связь. И разрыв этой связи допускается только при переходе от общего имени к конкретному. Например так: auto hosts = local_network.GetIterator();. И ведь тут с полпинка все становится понятно, даже думать не надо.


    4) Очень локальный вопрос стоит ли писать else, если ниже нет другого кода ниже:

    Ветвление всегда подразумевает ровно один прыжок или продолжение исполнения кода. Иногда ветвление подразумевает два прыжка: или прыжок в начало альтернативной ветви, или прыжок из конца основной ветви за пределы кода ветвления. Оптимизатор сам выбирает лучший вариант реализации ветвления, более выгодную основную ветвь и от писателя в этом процессе мало что зависит. Но для читателя ветвление и циклы всегда подразумевают очень большое усложнение кода. else стоит писать только тогда, когда без него иначе невозможно.
    И вот почему
    При чтении кода важно чтобы код был понятен читателю. Когда в коде появляется ветвление, читатель вынужден раздвоить контекст читаемого кода для себя. Это всегда сложно. Если читатель видит только одну ветвь в ветвлении, второй контекст читателю дастся легче. Если читатель видит что у ветвления есть две ветви, они будет вынужден напрячься чтобы поддержать сразу два контекста в параллели. И если в конце окажется что вторая ветвь ветвления - это лихо замаскированная линейная часть остатка кода до конца подпрограммы, у читателя снова появятся большие вопросы к целям такого изложения кода.


    6) Писать ли пробел между стандартными функциями и скобками:

    Пробелы нужны для разделения связанных цепочек символов - слов. Код - это запись рассуждений автора о том, что должна делать программа. Код должен читаться как рассказ, в котором слова правильно разделены между собой и правильно расставлены смысловые акценты.
    И вот почему
    Пробелы нужны чтобы отделить одно от другого. С какой целью? Наверное с целью обратить внимание читателя на то, что пробелами отделено. Пробелы сами не являются акцентами, но позволяют акцентировать внимание читателя, в то время как любые другие символы только забирают на себя внимание потому что читателю надо понять смысл присутствия символов в месте их присутствия.
    a==5 - никаких акцентов, ничего не видно. Даже с подсветкой синтаксиса 5 и == читаются плохо и практически неотличимы от a=5 при беглом чтении. В такие моменты у читателя в контекст вносится ошибка или, как минимум, неопределенность ошибки. Но основная цель писателя кода - это написать понятный для чтения код. Поэтому через пробелы надо акцентировать внимание читателя именно на символе эквивалентности - a == 5, позволяя ему правильно прочитать написанное при беглом чтении.
    if(a == 5){ - в этом коде видно только акцент на знаке эквивалентности, но не на выражении условия. if (a == 5) { - уже лучше, но скобки требуют от читателя понять природу их нахождения, что это именно условие, а также вчитаться в левый и правый аргументы условия. if( a == 5 ){ - здесь для читателя акцент поставлен именно на всем условии, теряется только знак начала области видимости - {. И именно поэтому египетские скобки - это плохо. Область видимости должна начинаться на своей строке, потому что для нее нужно создать максимально заметный акцент.
    for (int a = 0; a < 10; a++) { - тут акценты созданы, но не так, чтобы читатель легко прочитал тип счетчика или операцию шага. for( int a = 0; a < 10; a++ ) - а вот тут внимание читателя акцентируется именно на выражении счетчика. И читателю уже не надо выискивать глазами условия, инициализацию и шаг. Это все выделено пробелами и подано для самого комфортного чтения.


    7) Тот же вопрос только про функции, что я сам написал:

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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В контексте использования директивы #include есть два важных принципа.
    Важный из этих двух сейчас - это принцип Include What You Use.

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

    В обычном проекте считается что один файл исходного кода соответствует одному модулю трансляции. В более сложных проектах, где используется SCU/Unity Build, один модуль трансляции уже представлен несколькими файлами исходного кода. Но IWYU требует соблюдать правила только для модуля трансляции, а не для файлов исходного кода.
    Эта ситуация обозначает одну из проблем соответствия IWYU, которую не замечают многие разработчики, кто пользуется IWYU и SCU одновременно. Дублирование директив, нарушение порядка от общего к частному и доступность директив для всего нижележащего кода других файлов исходного кода сводит на нет весь принцип IWYU.
    Простейшей ошибкой наивного следования IWYU при использовании SCU является простой забытый инклуд, который присутствует в другом файле исходного кода, размещенном модуле трансляции выше по коду. SCU формируются динамически, на базе определенного алгоритма. Сегодня ошибки нет, а завтра исходный код попадет в другой модуль трансляции, где выше по коду уже не будет нужного инклуда и трансляция пойдет прахом. Особой пикантности данной ситуации добавляют разные сценарии SCU для разработчиков и систем CI. В этом случае ошибку с потерянным инкудом искать будут очень долго и не в одно лицо. Такие ситуации реально существовали у меня на работе. Особую пикантность такой ситуации добавляет и то, что на этой работе люди особо активно пропагандировали IWYU, говоря о безмерной пользе принципа вместе с использованием SCU.
    Решением же является понимание несовместимости использования SCU и IWYU в одном проекте.

    Излишне детальное следование IWYU.

    Принцип IWYU говорит включать только то, чем пользуешься. Что это означает в деталях?
    Когда заголовочный файл использует какое-то объявление, он должен сперва подключить соответствующий заголовок. Когда исходный код использует какое-то объявление, он должен сперва подключить соответствующий заголовок.
    Код не пишется в вакууме, код пишется в составе библиотек и систем, в рамках которых код связывается между собой. На более мелком уровне код пишется в составе подсистем и модулей, а подсистемы и модули уже формируют более глобальные системы и библиотеки. В рамках всех этих единиц агломерации кода сам код является тесно связанным между собой. Заголовки одной подсистемы редко не будут перекрестно включать друг друга потому что активно пользуются их объявлениями для реализации функционала подсистемы.
    Внешний же код, использующий функционал подсистемы, редко будет ограничиваться включением только одного заголовка, потому сами заголовки включают друг друга и через IWYU диктуют подобный подход своему пользователю.
    Это заставляет блоки включений раздуваться до невиданных масштабов в сотни строк одних только #include. Если проект использует #pragma once, то огромные блоки включений сказываются только на удобстве чтения, сводя удобство разбора инклудов к нулю. Но если проект использует только define-guard, то от огромных блоков инклудов начинает катастрофически страдать скорость сборки. В этот момент люди обычно вспоминают про SCU и с его помощью окончательно хоронят гибкость проекта в пучине ошибок совместного использования SCU и IWYU.
    А решением в данном случае является выделение для единиц агломерации кода своих собственных публичных заголовков, внутри которых в правильном порядке будут подключены все заголовки используемого кода.
    Этот простой шаг кратно сокращает списки инклудов в пользовательском коде, не противоречит IWYU и позволяет на более высоком уровне организовать публичный интерфейс модуля, подсистемы, системы или библиотеки.


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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для начала стоит разобраться в том, написан ли код в вопросе на OpenGL - нет, не написан.
    Код в вопросе использует обертку GLFW, которая довольно сильно абстрагирует пользователя от самого по себе OpenGL. Отсюда у тебя и непонимание самых основ работы с OpenGL, а ведь самым основным в этой работе является управление контекстом устройства.
    Коротко описать механизмы работы с контекстом не получится, в трех словах половину документации API не пересказать. Но есть источники, которые обязательно нужно изучить: [1], [2], [3], [4].

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

    Эта информация и является решающей в твоем случае. Конструктор твоего класса Window не даром имеет третий параметр - reference. Ты не мог ввести его специально и не знать о том, что я сейчас напишу.
    Если обратиться к документации на glfwCreateWindow[?], то можно узнать что пятым параметром функции заявлено The window whose context to share resources with, т.е. окно, с контекстом которого следует разделить таблицу ресурсов.

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

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

    Исходя именно из этих преимуществ и следует выбирать между функтором и функцией.
    Скажем, если бы нужно было nums вписать в CSV таблицу в виде матрицы, то проще использовать функтор. Создать его, настроить поток вывода, символ-разделитель столбцов, количество выводов до перехода на следующую строку и передать в std::for_each.
    Если такая настройка поведения не требуется, от функтора лучше отказаться в пользу функции во всех случаях.

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

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

    Самое первое - это не нужно именно получать координаты курсора в момент его обработки. Нужно реагировать на сообщения о перемещении курсора по окну и запоминать координаты в его клиентской области, в которой у тебя и происходит отрисовка сцены.
    Но если очень хочется, то на WinAPI можно использовать GetCursorPos[?] и ScreenToClient[?] чтобы на месте получить координаты курсора в клиентской области окна.
    На этом этапе нужно принять во внимание то, в какой координатной системе считаются координаты курсора.
    Например, в WinAPI координатная система окна выглядит так. И именно в ней считаются координаты курсора.

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

    Следующим этапом будет переход от клиентской области к области вьюпорта. Этот переход делается через дополненную 2х2 матрицу трансформаций 2D-пространства. Ее важно использовать везде, т.к. это позволит заложить фундамент однообразного перехода из пространства клиентской области экрана в пространство координатной системы камеры.
    Что важно на этом этапе. Пространство вьюпорта и пространство камеры связаны через пространство нормализованных координат области отсечения. В разных GAPI пространство области отсечения представлено по-разному. И пространство вьюпорта тоже по-разному представлено в разных GAPI.
    Например, в OpenGL координатная система вьюпорта выглядит так.
    Еще на этом этапе нужно заложить переход от точки к лучу, т.к. вьюпорт уже имеет объем. Для этого координаты курсора нужно дополнить минимальной и максимальной глубиной вьюпорта, получив уже две точки координат курсора.
    В качестве примера такого преобразования можно взять код функции D3DXVec3Unproject прямо из Wine. Там делается правильный и простой переход из пространства клиентской области окна в пространство области отсечения OpenGL через пространство вьюпорта.

    От пространства области отсечения координаты курсора нужно перевести в пространство сцены.
    Матрица View-Projection переводит геометрию из системы координат пространства сцены в систему координат области отсечения. Значит, чтобы сделать переход из области отсечения в пространство сцены, нужно использовать инверсию матрицы View-Projection.

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

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

    Что это означает на практике? В языке есть термин массива статического размера. Частным примером такого массива является строковой литерал. Конкретно у строкового литерала из простых символов тип будет таким: const char[N], где N - это размер памяти под строковой лиерал, в байтах.

    Таким образом, если сделать такое объявление шаблона
    template< typename TValue >
    void Foo( TValue& value ); // (1)

    то при инстанцировании как Foo( "Hello" ); аргумент TValue определится как const char[6].
    И мы довольно легко можем воспользоваться этим механизмом. Нужно только дать пояснение, как и во что стоит выводить шаблонные аргументы.

    Шаблон можно записать вот так:
    template< typename TValue, size_t LENGTH >
    void Foo( TValue (&value)[ LENGTH ] ); // (2)

    В этом случае при инстанцировании как Foo( "Hello" );, аргумент TValue будет выведен как const char, а нетиповой аргумент LENGTH будет выведен в значение 6.
    Собственно, все. Размер переданного массива получен.

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

    В чем суть модификаторов в параметрах шаблона

    Если первый шаблон объявить как
    template< typename TValue >
    void Foo( TValue value );

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

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

    Параметр такой "другой" функции лучше всего определить универсальной ссылкой и использовать идеальную передачу. Тогда все будет работать правильно.
    Ответ написан
    2 комментария
  • Как грамотно переписать фабрику?

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

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

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

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

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

    В сложных высокотехнологичных движках множество моделей используют собственную память для представления акторов, вводя многократную избыточность использования памяти, но позволяя работу в полностью параллельном режиме на нескольких потоках. В таких движках существует отдельная стадия сборки представлений из разных подсистем в единое представление актора с попутным разрешением коллизий, если они возникают.
    Тебе подобные сложности точно не нужны на текущем этапе. Реализовать можно просто парой-тройкой моделей (графика, столкновения и физика) поверх единой системы объектов мира, обработку которых пустить последовательно в одном потоке.
    Ответ написан