Ответы пользователя по тегу C++
  • Ошибка при создании явной специализации. В чём ошибка?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    template <typename T>
    T maxn(T arr[], int arrSize);

    Это - общая форма шаблона функции. Если тебе нужно сделать явную специализацию шаблона, твоя явная специализация должна соответствовать этому общему шаблону.

    template <>
    const char* maxn(const char* arr[], int arrSize);

    Сырые однобайтовые строки в C++ (при соблюдении правила нуль-терминации) определяются типом char* или const char* если строку не планируется модифицировать. Наша функция не планирует модифицировать строки, следовательно явная специализация шаблона должна работать с типом const char*.
    Тип const char* должен быть подставлен везде вместо параметра шаблона T.

    Таким образом явная специализация шаблона для сырых строк будет полностью соответствовать общему шаблону функции.

    А теперь немного о твоей ошибке.
    template <> char* maxn(const char *arr[], int arrSize);

    Это объявление не является явной специализацией приведенного выше шаблона. Это - определение перегрузки функции через явную специализацию какого-то иного неописанного шаблона. Это потому что результат функции char* отличен от параметра функции const char*.
    В целом, эта запись является плохо сформулированной (ill-formed) по целому ряду причин, включая и то, что перегрузка не может осуществляться через изменение типа результата функции.

    В целях обучения я советую не опускать перечисление аргументов явной специализации шаблона:
    template <>
    const char* maxn<const char*>(const char* arr[], int arrSize);

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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Это простой способ поделить строку на подстроки.
    inline std::vector<std::string_view> SplitString( std::string_view input_string, const char separator )
    {
    	std::vector<std::string_view> parts;
    
    	size_t part_length = 0;
    	while( ( part_length = input_string.find( separator ) ) != input_string.npos )
    	{
    		parts.emplace_back( input_string.data(), part_length );
    		input_string.remove_prefix( part_length + 1 );
    	}
    
    	if( !input_string.empty() )
    	{
    		parts.push_back( std::move( input_string ) );
    	}
    
    	return parts;
    }
    Ответ написан
    2 комментария
  • Какие языки совместимы с C++?

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

    Тебя же интересуют встраиваемые языки.

    Взять, например, D. Он полностью совместим с C++. Однако, его требования не позволяют простую линковку объектных файлов C++ и D в один бинарный файл. На C++ или должен быть создан D Runtime (что не так просто сделать), или C++ код должен линковаться в бинарник на D, в качестве Better C++. Т.е. получается наоборот, в базе будет D, а C++ его только расширяет.
    С питоном и Java все точно так же. C# вообще работает с библиотеками классов, написанными на C++/Cx, т.е. на модифицированном C++.
    Эти языки не являются встраиваемыми. Их возможности расширяются за счет использования C++.

    Хорошим ответом на твой вопрос будет вот такой список встраиваемых языков. Из этого списка можно брать любой язык, ядро которого написано на C или C++.
    Мой личный выбор - это AngelScript и Lua/Terra.
    Ответ написан
    Комментировать
  • Android NDK Error: no matching member function for call to 'push_back'?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    class rck{
    public:
        unsigned short x,y;
        void show();
        int tm=0;
        uint8_t type=0;
    };

    Вольное комментирование состояния кода
    Тут у нас большие проблемы с читаемостью кода. stdint соседствует с сырыми конструкциями модификации типа, методы вперемешку с полями, класс вместо структуры, использование запятой при объявлении полей, имена в одну букву и акронимы. Неинициализированные поля - распространенный источник ошибок.

    Лучше будет с молоду учиться правильно оформлять свой код. Например так.
    struct rck final
    {
    	uint16_t x = 0;
    	uint16_t y = 0;
    	int32_t tm = 0;
    	uint8_t type = 0;
    	
    	
    	void show();
    };

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


    У тебя объявлен, вроде бы, агрегатный тип с неявным конструктором по умолчанию, в котором выполняется инициализация только полей tm и type.

    rock.push_back({(uint16_t)(x*64),(uint16_t)(y*64),0,0});

    Тут перед обращением к push_back формируется фиктивный std::initializer_list<uint16_t> с двумя элементами внутри.
    В общем смысле, до полного списка аргументов нужно указать еще два аргумента. Тогда эту запись можно будет считать агрегатной инициализацией временного объекта типа rck.

    Такая форма агрегатной инициализации стандартизирована в C++11. До C++14 агрегатная инициализация отключается для типов, в которых присутствует инициализация полей по месту объявления (в коде это поля tm и type). Каждый следующий стандарт требования к агрегатной инициализации только ужесточает.

    GCC давно славится своей слабой поддержкой стандарта языка. Это означает что если код проходит трансляцию g++, это далеко не факт что он соответствует стандарту.
    NDK сегодня для сборки использует clang, который на данный момент считается максимально приближенным к стандарту транслятором.

    Все это должно дать понять что данный код не соответствует стандарту языка и не должен компилироваться. Подходящего конструктора от двух аргументов для типа rck не определено и тип не является агрегатным.
    То, что g++ смог собрать этот код, останется на совести g++.
    Еще одним важным моментом будет точное понимание, какой именно стандарт языка выбран для трансляции твоего кода.
    Ответ написан
    1 комментарий
  • Что лучше использовать #include или LoadLibrary?

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

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

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

    Директива препроцессора #include[?] лишь косвенно относится к вопросу. На самом деле к вопросу относится директива #pragma comment( lib )[?].
    Прагма работает только для cl - компилятора от Microsoft. Подключаемая таким образом статическая библиотека должна находиться в перечисленных Library Path, в настройках сборки проекта. Эта прагма является альтернативой прямого указания статической библиотеки в настройках сборки проекта.
    Эту директиву любят использовать в сторонних заголовках, код для которых поставляется только в бинарном виде, в статических или динамических библиотеках. Вместе с динамической библиотекой такого решения поставляется и библиотека статическая, в которой находятся инструкции для неявного связывания с динамической библиотекой. Результатом работы #pragma comment( lib ) с такой статической библиотекой будет неявное связывание твоего кода с указанной динамической библиотекой.

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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Drottarutarnum, p = { s }; - это Copy Initialization.
    В буквальном смысле, что тут происходит. Сперва ты создаешь temporary local через конструктор Protocol::Protocol(Serial &s). Следом, через operator =, который сводится к конструктору копирования/перемещения, ты делаешь Copy Construction объекта p. Делается это через конструктор копирования/перемещения по умолчанию, который просто копирует/перемещает поля используя их конструкторы копирования/перемещения.

    Лямбда создается внутри temporary local значения, т.к. именно для него вызывается соответствующий конструктор. Далее эта лямбда остается жить в объекте Serial, а сам temporary local уничтожается сразу же после конструирования p.

    После этого в замыкании лямбды остается dangling pointer на уже уничтоженный this.

    Одним из возможных решений данной проблемы может быть не p = { s };, а p{ s }; или, как ты уже описал, p = s;. Такой тип инициализации уже называется Value Initialization. Но по своей сути это только отсрочка проблемы. Тебе придется очень пристально следить за жизнью такого объекта, т.к. вероятность обрести все тот же висячий указатель в замыкании в следствии неявного копирования крайне высока.

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

    В качестве еще одного способа - научить Serial отцеплять лямбду уничтожаемого Protocol и написать правильный конструктор копирования Protocol, соблюдая правило 3/5/0.

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

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

    [&p, &all_files_and_sums]
    Если с захватом all_files_and_sums по ссылке я не спорю, то точно ли p продолжит существовать после выхода из итерации? Давай подумаем. А после выхода из цикла продолжит?
    У меня вообще нет уверенности в том, что после смещения bfs::directory_iterator адрес возвращаемого им bfs::directory_entry изменится. Я бы предпочел захватывать копию bfs::directory_entry в лямбде.

    У тебя нет ожидания окончания работы от пула потоков - нет синхронизации с завершением запланированных задач. Точнее... ну как нет... У тебя жесткая синхронизация через std::future::get[?]. По факту в этот момент у тебя блокируется главный поток до момента обработки запланированной задачи. У тебя всегда планируется только одна задача. А видимость конкурентной работы создается лишь потому что какой поток ее схватил, тот и работает. Видимо задачи у тебя быстро обрабатываются, раз ты глазами этого не увидел.

    Тебе стоит сохранять сами std::future от задач. В момент планирования их результат еще не определен и get вызывать не надо. Надо дождаться завершения работы всех потоков в пуле и исчерпания всех задач. Для этого у тебя в пуле должны быть продуманы механизмы оповещения.
    После обработки всех задач ты можешь вызывать std::future::get, получать результаты и производить свои операции над ними.
    Альтернативно, ты можешь более тонко реагировать на завершение каждой задачи и появление в ее std::future результата. Это тоже можно сделать. Просто сделать это надо своими руками и продумав масштабируемость такого механизма.

    И в дополнение. Зачем тебе boost? Ты пользуешься std::future и лямбдами, ты пишешь в рамках стандарта C++11. Тебе доступны и std::thread, и все примитивы барьерирования из std.
    В твоем распоряжении вся Thread Support Library. А boost тут явно лишний.
    Если переключишься на C++17, то тебе и boost::filesystem не будет нужна, т.к. станет доступна std::filesystem - Filesystem Library.
    Ответ написан
    Комментировать
  • Как правильно передать в RegSetValueEx в lpData NULL?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Начнем с простого.
    sizeof( expresion ) позволяет узнать минимально допустимый размер памяти, требуемый для того чтобы память могла вместить объект с типом результата утверждения expresion.
    sizeof( value ) вернет размер LPCWSTR, т.е. размер типа const wchar_t*.

    wcslen не допускает передачу нуля в качестве своего параметра. Забота об этом лежит полностью на тебе.

    RegSetValueExW может принимать нуль в параметре lpData, но тогда cbData обязан содержать 0. Но в целом, это очень плохая практика. Если тебе нужно записать пустую строку, пиши пустую. строку. Т.е. "", но не нуль.

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

    Далее, по самому коду. Его тяжело читать.
    Как читать будет легче
    bool modifyKey( HKEY root_hey, const std::wstring_view& path, const std::wstring_view& key_name, const std::wstring_view& new_value )
    {
       HKEY folder_key;
    
       if( RegCreateKeyW( root_hey, path.data(), &folder_key ) != ERROR_SUCCESS )
       {
          return false;
       }
    
       const BYTE* const value_buffer = reinterpret_cast<const BYTE*>( new_value.data() );
       const DWORD buffer_size = static_cast<DWORD>( new_value.length() * sizeof( wchar_t ) + 1 );
       if( RegSetValueExW( folder_key, key_name.data(), 0, REG_SZ, value_buffer, buffer_size ) != ERROR_SUCCESS )
       {
          RegCloseKey( folder_key );
          return false;
       }
    
       if( RegFlushKey( folder_key ) != ERROR_SUCCESS )
       {
          RegCloseKey( folder_key );
          return false;
       }
    
       RegCloseKey( folder_key );
       return true;
    }
    Ответ написан
    7 комментариев
  • Как создать папку с учетом имени пользователя c++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для современных версий ОС Windows в наборе WinAPI присутствует функция SHGetKnownFolderPath[?].
    Она позволяет получить путь к некоторым предварительно определенным папкам, набор которых описан абстракцией KNOWNFOLDERID[?].

    Тебе подойдет FOLDERID_Desktop для получения папки рабочего стола пользователя.
    При работе с этой функцией очень важно не забывать пользоваться функцией CoTaskMemFree[?] для освобождения памяти, переданной из функции через указатель ppszPath.

    Небольшой пример использования
    wchar_t* path_buffer = nullptr;
    if( FAILED( ::SHGetKnownFolderPath( FOLDERID_Desktop, KF_FLAG_DEFAULT, 0, &path_buffer ) ) )
    {
    	::CoTaskMemFree( path_buffer );
    	LOG_ERROR( LOG_CHANNEL, "Failed to get known directory; error - #{:08X}.", ::GetLastError() );
    	// terminate ...
    }
    
    std::wstring user_desktop_path{ path_buffer };
    ::CoTaskMemFree( path_buffer );
    
    // whatever with `user_desktop_path` ...
    Ответ написан
    Комментировать
  • Android как создать папку средствами NDK?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Через NDK доступна функция mkdir[?].
    Ее поведение для Android ни чем не отличается от изложенного в официальной документации.

    Для проверки существования папки можно использовать функцию stat[?], а можно опереться на код ошибки EEXIST из mkdir.
    Ответ написан
    Комментировать
  • Как правильно передать в метод массив и получить обратно массив другого размера?

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

    Для начала можно остановиться на векторе. Еще одним важным типом будет std::span[?] или gsl::span[?][S] если ты не можешь пользоваться C++20.

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

    Вот объвление твоей функции: gsl::span<float> Метод( gsl::span<float> values ).
    span - это не владеющий памятью тип, обозначающий участок непрерывной памяти с данными определенного типа. span очень легок и является value type - т.е. создан чтобы его передача по значению не приводила к ощутимым нагрузкам. span конструируется из std::vector, std::array, плоских массивов, std::unique_ptr<[]> и сырых блоков памяти.

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

    Псевдокод
    std::span<float> Метод( std::span<float> values )
    {
    	size_t processed_count = 0;
    	for( float& element : values )
    	{
    		// ... обработка значений
    		// ... изменение processed_count
    		// ... условия обрыва цикла
    	}
    	
    	return values.subspan( 0, processed_count );
    }
    
    int main()
    {
    	std::vector<float> values{ 7.83f, 14.1f, 20.3f };
    	std::span<float> processed_values = Метод( values );
    	
    	for( const float& value : processed_values )
    	{
    		std::cout << value << ' ';
    	}
    	
    	return 0;
    }
    Ответ написан
    2 комментария
  • Какие требования предъявляются разработчику на с++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    $1.5k/mth - это довольно серьезная оплата для специалиста. Такую з/п начислят линейному разработчику (Middle Engineer). В области работы с C++ все очень индивидуально и требования сильно пляшут даже не между компаниями, а между командами и проектами внутри компаний. Однако, средний набор требований выделить все-таки можно.

    За эти деньги от тебя потребуется глубокое знание стандарта C++ и способность быстро восполнить любой пробел в своей компетентности. Тебя допустят к ревью младших сотрудников, где от тебя будет требоваться, в первую очередь, отлов глубоких нарушений стандарта. Например, таких как нарушение алиасинга, нарушение передачи ресурса, нарушение времени жизни, потенциальные утечки ресурсов и памяти, потенциальное или явное неопределенное поведение. Также от тебя потребуется детальное понимание различий между версиями стандарта.
    От твоей экспертизы в стандарте будет зависеть скорость реализации функционала. Не только от одного тебя тебя, но и от тебя тоже.
    Это - твой Hard skill requirement. Алгоритмическая база и экспертиза в математике тоже имеют свой вес в профессиональных навыках. Но без глубокого знания стандарта, будь у тебя хоть Нобелевка по математике, толку на роли m.e. с тебя будет мало.

    И это - всего одна треть требований к тебе на такой позиции.

    Второй третью требований к тебе будут хорошо развитые социальные навыки. Это 10 лет назад при собеседовании смотрели только на твои профессиональные навыки. Сегодня профессиональные навыки иногда ставят на второе место перед навыками социальными.
    Каким бы крутым специалистом ты ни был, если ты токсичен, несдержан или неадекватен - твое появление в команде будет нежелательным. Бывают разные случаи. Бывает, адекватный человек заходит в команду токсиков и берет все в свои руки. Бывает, один свеженабранный неадекват уничтожает команду и продукт за период испытательного срока.
    Когда тебя допускают к ревью других людей, тебя самого тоже ставят на ревью, но уже в другой плоскости. Если твои слова будут негативно влиять на других сотрудников, к тебе появятся вопросы социального плана.
    Это - твой Soft skill requirement.

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

    Не стоит сегодня надеяться на то, что лишь за способность писать код тебе будут платить больше $800/mth. Хотя, конечно же, на нашем рынке труда существуют и на такое готовые предприятия.
    Ответ написан
    Комментировать
  • Что не так с работой unordered_map?

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

    Если в шаблоне алокатора не описано правило смены аллоцируемого типа (A::template rebind<U>::other), по умолчанию при смене аллоцируемого типа будет заменен первый шаблонный параметр. Был у нас Alloc<Foo, ...>, станет Alloc<Bar, ...>.
    Это означает что для правильной передачи состояния аллокатора нужно предусмотреть конструктор преобразования из аллокатора от твоего шаблона, но с другим первым аргументом.

    Еще можно использовать Polymorphic Allocator, но для этого потребуется сменить стандарт на C++17.
    Эту стратегию не так просто описать, поэтому я прибегну к ссылкам на доклады по этой теме.

    CppCon 2017: Bob Steagall “How to Write a Custom A...
    051. Modern C++ Allocators – Руслан Арутюнян (Intel)
    Taming dynamic memory - An introduction to custom ...
    C++Now 2018: David Sankel “C++17's std::pmr Comes ...
    Это, конечно, далеко не всё на данную тему. Но цели выписывать все у меня и нет. Я привел ссылки, которым доверяю в плане чистоты информации.
    Советую просто пройтись по хабру и ytube поиском докладов и статей.

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

    Но если ты работаешь со стандартом до C++11, то у тебя аллокатор вообще не может иметь состояние.
    All custom allocators also must be stateless. (until C++11)
    Ответ написан
    Комментировать
  • Большие шаблонные классы( реализуя CRTP). Все пихать в один .h файл?

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

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

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

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

    По поводу организации кода.
    Код шаблонов точно так же можно организовать в наборы файлов. Для шаблонов точно так же разрешена и дружественность, и предварительное объявление, и определение не по месту объявления.
    Реализацию шаблонов можно развести между заголовками (.h-файлами) и файлами встраиваемых реализаций (.inl-файлами).
    При этом важным остается правило доступности шаблона из места его инстанцирования. Т.е. вся группа файлов с реализацией шаблона должна быть как-либо связана с заголовком объявления шаблона, который далее планируется предоставлять пользователю шаблона.
    Ответ написан
    2 комментария
  • Как ускорить этот код(клеточный автомат)?

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

    8Б на клетку, состояние которой может поместиться в 1Б, это действительно огромный размер.
    enum CellState : uint8_t уменьшит размер состояния с 4Б до 1Б. А еще этот тип стоит переименовать, т.к. это не CellState, а что-то относящееся к поведению клетки. А вот CellState будет выглядеть так:
    // Renamed from `CellState`.
    enum CellBehavior : uint8_t
    {
        Empty,
        Alive,
    };
    
    struct CellState final
    {
    	CellBehavior	current_behavior : 4;
    	CellBehavior	next_behavior : 4;
    };

    Это позволяет уменьшить размер клетки до 1 байта.

    Данные оперативной памяти процессор подтягивает к себе во внутренний кэш. Кэшей у процессора много и все они связаны. Кэш процессора поделен на линии, работа с которыми синхронизируется между ядрами процессора. Вот именно тут появляется два термина: False cacheline sharing и True cacheline sharing. Если "False", то обрабатываемые разными ядрами данные разделены в разные кэш-линии. Когда "True" - требуемые разным ядрам данные находятся в одной кэш-линии и привет синхронизация. А это ой как медленно.

    В каждом процессоре сегодня сидит гадалка, которая предсказывает какие тебе надо подтянуть данные из RAM в CPU Cache. Выборка из RAM - это довольно долгая процедура, поэтому нужна гадалка чтобы предсказать что судьбой твоего алгоритма предначертано выбрать на следующем этапе. Бывает что гадалка ошибается и тогда твой лагоритм встает в синхронизацию до завершения нужной выборки из памяти. А это - еще медленнее чем синхронизация по кэш-линиям. Это называется промахом по кэшу - cache miss.
    К счастью, это не гадалка виновата в своей ошибке, а ты просто неправильно написал лагоритм. Вот чтобы из лагоритма сделать алгоритм, следует озаботиться чтобы он был более лоялен к гадалке и кэшу процессора.

    Докину еще немного полезной информации.
    Сходи к Адаму Мартину и к Unity, посмотри на парадигму ES/ESP/ECS. Изучи DOD. Попробуй реорганизацию из твоего текущего потока сущностей с полями в потоки полей сущностей. Переделай батчинг обработки клеток так, чтобы данные не синхронизировались между ядрами процессора.
    Возможно тебе еще поможет понимание подхода Out of line, т.к. там хорошо объясняется почему очень большие объекты при их поточной обработке - это не очень дружественно кэшу процессора.
    Еще сюда можно добавить информацию о автоматической векторизации. Это позволит задействовать SIMD инструкции для твоего кода. DOD очень элегантно ложится для обработки твоих клеток SIMD командами.

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

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

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

    В результате подмены сигнатуры с BOOL (__stdcall *)(HANDLE, PBOOL) на bool (__stdcall *)(void*, bool*) компилятор думает об одном размере стека, а код функции - о других.

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

    Однако, конкретно тут проблема у нас не в несовпадении типов, потому что.
    __stdcall свой результат передает через регистр. регистр используется целочисленный или вещественный. Для целочисленного регистра используется правило продвижения типа. Это означает, что функция, записав значение типа BOOL (размер 4Б) ничего не испортит пользовательскому коду, который прочитает из регистра все 4Б с учетом правила продвижения.

    Реальная проблема кроется в том, что указатель на однобайтовое целое (bool*) передается в использование как указатель на четырехбайтовое целое (BOOL* или PBOOL). Вызываемая функция ведь имеет сигнатуру BOOL (__stdcall *)(HANDLE, PBOOL) и со вторым параметром работает как с 4Б целым по указателю.
    Именно это и приводит к порче стека и тебе, автор, сильно повезло что ты запускаешься в отладке, где каждое значение на стеке обрамлено заборчиком, за сохранностью которого всегда приглядывает специальный сервисный код между обращениями к подпрограммам.

    Именно такой заборчик рядом с isWow64 и был поломан в результате вызова IsWow64Process с параметром неподходящей длины. Измени тип isWow64 на BOOL и все станет нормально, даже хендл "kernel32.dll" потом сможешь нормально освободить.
    Ответ написан
    5 комментариев
  • Как возможно задать параметры, а именно текстуру отдельно для каждого треугольника?

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

    Развернуто:

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

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

    Под материалом понимается комплекс из программы и ресурсов, которые отображаются на геометрии.
    Материал отображает на геометрию заданный своими свойствами набор текстур и параметров. С точки зрения геометрии, материал - это DIP (Draw Indexed Primitive - твой glDrawElements). С точки зрения шейдера, материал - это комплекс из программы шейдера и набора входных/выходных параметров этой программы.
    Один материал одной геометрии - это один DIP с установкой набора текстур и параметров материала, а так же - с установкой нужного буфера индексов, вершин и инстансов.

    Ты хочешь чтобы Один набор треугольников твоей плоскости имел одну текстуру, а другой - другую. Вот и организуй два материала, у которых одна шейдерная программа, но две разные текстуры. Далее тебе надо ввести больше вершин, т.к. с вводом материала и текстурных координат, с учетом твоих требований, твои 4 вершины больше не могут обеспечить свою уникальность. В тех позициях плоскости, где два смежных треугольника отображают разные текстуры, тебе требуется ввести по две вершины вместо одной.
    Далее - организуй индексный буфер так, чтобы примитивы в нем описывались непрерывной последовательностью для каждого материала отдельно. Т.е. сперва все треугольники с одним материалом, потом все треугольники - с другим.
    Под конец определи инструкции для своей геометрии, сколько DIP-ов согласно твоим требованиям должно быть выполнено, для каких материалов и на какой геометрии.

    Таким образом ты реализуешь очень простую систему поддержки материалов и выполнишь свою задачу.
    Ответ написан
    Комментировать
  • Как правильно использовать anonymous namespace?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Если хочется полностью исключить возможность использования приватных выражений, то использовать стоит не пространства имен, а класс с вложенными типами под соответствующими секциями.
    class a final
    {
    private:
    	class ex
    	{
    		int f = 0;
    	};
    	
    	static void foo() { std::cout << 'f' << std::endl; };
    	
    public:
    	class F
    	{
    	public:
    		F() { foo(); };
    		
    	private:
    		ex m_exes[4];
    	};
    };
    Ответ написан
    3 комментария
  • Несовместимые типы операндов?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    state ? (cout << b / a << endl, exit(0)) : state = (a == 0) && (b == 0);

    Не стоит так писать. Мало того что это выражение не укладывается в синтаксис, так еще оно из себя представляет слепую зону, код в которой невидим. Никому в голову не придет читать exit(0) или конструкцию присвоения внутри тернарного оператора.

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