Русский
Открыть в Telegram
ГГрокаем C++

Грокаем C++

✅ Высокое доверие
@grokaemcpp · канал · Технологии · в индексе с 21.04.2026
9 349подписчиков+6 за неделю
1 576средний охват поста
16.9%ER — охват к подписчикам
12постов за 30 дней
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Базовые трейты объектов #новичкам Трейты(или свойства объектов) - мощнейший инструмент проверки требований для исполнения кода. Вы явно в апи через sfinae или концепты можете задавать ограничения на множество типов, с которыми хотите работать, производя проверку в compile-time. Сегодня обсудим для самой простой структуры, какими свойствами классов она удовлетворяет. Чисто, чтобы вкатиться в базовые стандартные трейты. Проверить свойства типа можно довольно просто. static_assert + использование самого трейта: struct A { int a; int b; int c; }; // --- true --- static_assert(std::is_class_v<A>, "A is a class"); static_assert(std::is_aggregate_v<A>, "A is aggregate"); static_assert(std::is_standard_layout_v<A>, "A is standard-layout"); static_assert(std::is_trivially_copyable_v<A>, "A is trivially copyable"); static_assert(std::is_trivially_destructible_v<A>, "A trivially destructible"); static_assert(std::is_trivially_copy_constructible_v<A>, "A trivial copy ctor"); static_assert(std::is_trivially_move_constructible_v<A>, "A trivial move ctor"); static_assert(std::is_trivially_copy_assignable_v<A>, "A trivial copy assign"); static_assert(std::is_trivially_move_assignable_v<A>, "A trivial move assign"); static_assert(std::is_default_constructible_v<A>, "A default constructible"); static_assert(std::is_trivially_default_constructible_v<A>, "A trivial default ctor"); static_assert(std::is_object_v<A>, "A is object"); static_assert(std::is_compound_v<A>, "A is compound"); // --- false --- static_assert(!std::is_empty_v<A>, "A is NOT empty (has members)"); static_assert(!std::is_polymorphic_v<A>, "A is NOT polymorphic"); static_assert(!std::is_fundamental_v<A>, "A is NOT fundamental"); static_assert(!std::is_scalar_v<A>, "A is NOT scalar"); Вообще эти трейты - это некая шаблонные структуры. У них есть статический constexpr член value, который обращается в true, если свойство присутствует у типа, и в false если нет. Для каждого трейта опре… Читать полностью →
31 · 2.7K ·
Г
Грокаем C++
Базовые трейты объектов. Ч2 #новичкам В прошлый раз у нас была довольно простая С-like структура, для которой мы смотрели стандартные свойства. struct A { int a; int b; int c; }; Сегодня посмотрим, какие свойства типа изменятся, если совсем чуть-чуть изменить структуру. 🔀 Добавим просто std::string в качестве поля структуры. struct A { int a; int b; int c; std::string s; }; Из-за того, что у std::string нетривиальные специальное методы классов, то это автоматически делает нетривиальными специальные методы у A, поэтому отличие будет только в следующих трейтах: static_assert(!std::is_trivially_copyable_v<A>); static_assert(!std::is_trivially_destructible_v<A>); static_assert(!std::is_trivially_copy_constructible_v<A>); static_assert(!std::is_trivially_move_constructible_v<A>); static_assert(!std::is_trivially_copy_assignable_v<A>); static_assert(!std::is_trivially_move_assignable_v<A>); static_assert(!std::is_trivially_default_constructible_v<A>); 🔀 Добавим виртуальный метод struct A { int a; int b; int c; virtual void foo() {} }; Очевидно, тип сразу же стал полиморфным. Но и много чего потерял. Чисто по определению некоторых свойств полиморфный тип не может им удовлетворять. Это: 👉🏿 std::is_standard_layout - грубо говоря, тип становится невместим с С 👉🏿 std::is_trivially_copyable - тривиальное копирование подразумевает простое копирование по байтам. Копирование полиморфного типа нельзя производить побайтово. Компилятор обязан либо скопировать vptr как есть (если это тот же тип), либо установить правильный vptr нового типа (если копируем через базовый класс — slicing). Это уже нетривиальная логика. 👉🏿 std::is_trivially_default_constructible - конструктор по умолчанию не тривиальный, так как надо инициализировать vptr в правильную vtable. 👉🏿 std::is_aggregate - как только у класса появляется приватный метод(vptr в данном случае), он уже не может быть агрегатом. static_assert(std::is_polymorphic_v<Poly>); stati… Читать полностью →
19 · 2.2K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Безопасный memcpy #новичкам memcpy — это один из самых быстрых способов скопировать данные и один из самых быстрых способов получить UB. Функция принимает void* и size_t и радостно съест всё, что вы ей дадите: полиморфный тип, std::string, объект с нетривиальным деструктором. Компилятор не скажет ни слова, а программа сломается в самом неожиданном месте. В прошлых постах мы поговорили о трейтах. Для заданного типа они определяют, обладает ли тип определенным свойством или нет. Давайте применим трейты на практике. Ограничим через них доступ к самописной обертке memcpy. И в этом нам помогут концепты. Если трейты - это свойство типа, то концепт - это требование для типа. Определим концепт, который требует, чтобы тип был объектом(не функцией и не ссылкой) и был тривиально копируемым: template <typename T>  concept SafeForMemcpy = std::is_object_v<T> && std::is_trivially_copyable_v<T>; И вот теперь мы говорим в шаблонном функции, что хотим потребовать от типа выполнения условия SafeForMemcpy. Это можно сделать разными способами, вот самый простой: template <SafeForMemcpy T> void safe_memcpy(T* dest, const T* src) { static_assert(sizeof(T) > 0, "Cannot copy incomplete type"); std::memcpy(dest, src, sizeof(T)); } Вместо ключевых слов typename или class используется имя концепта. Теперь при использовании нетривиально копируемых типов будет появляться ошибка компиляции с нормальным сообщением о том, что тип не удовлетворяет концепту. В будущих постах будет разбирать концепты чуть подробнее. Require the best conditions. Stay cool. #cpp20 #template
20 · 2.4K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​std::find vs std::ranges::find #опытным С++20 библиотека диапазонов принесла нам много новых полезных алгоритмов и способов управления данными. Но не только это. Рэнджи задублировали и привычные нам "древние" стандартные алгоритмы. Хотя кажется "что какая разница, использовать новое апи или старое?" - разница все-таки есть. И сегодня поговорим об одном конкретном кейсе. Что делает std::find? Ищет элемент в промежутке между итератором начала и конца последовательности и возвращает на него итератор на этот элемент: auto it = std::find(vec.begin(), vec.end(), 42); Если элемент не найден, то find просто возвращает итератор на конец диапазона. Ровно такую же строчку можно написать с std::ranges::find и это заработает: auto it = std::ranges::find(vec.begin(), vec.end(), 42); Однако здесь открываются новые возможности. Все потому, что второй аргумент алгоритм - конец диапазона - не обязан быть того же типа, что и итератор начала: // std::find template< class InputIt, class T > InputIt find( InputIt first, InputIt last, const T& value ); // std::ranges::find template< std::input_iterator I, std::sentinel_for<I> S, class T, class Proj = std::identity > requires std::indirect_binary_predicate <ranges::equal_to, std::projected<I, Proj>, const T*> constexpr I find( I first, S last, const T& value, Proj proj = {} ); По шаблонным параметрам видно, что во втором случае first и last могут быть разных типов, в отличие от std::find. Это позволяет использовать кастомный тип ограничителя последовательности, который позволит сделать поиск несколько эффективнее. std::find реализован примерно вот так: template<class InputIt, class T = typename std::iterator_traits<InputIt>::value_type> constexpr InputIt find(InputIt first, InputIt last, const T& value) { for (; first != last; ++first) if (*first == value) return first; return last; } На каждой итерации цикла идет проверка, не дошли ли мы до конца. Это нужно,… Читать полностью →
12 · 2.7K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Виртуальный шаблонный метод #опытным Во влажных мечтах С++ программистов всегда находится место для возможностей одновременного использования статического и динамического полиморфизмов. И тут прям напрашивается иметь виртуальный шаблонный метод. Давайте напишем: struct Base { template <typename T> virtual void do_something(T arg) = 0; virtual ~Base() = default; }; struct Derived : Base { template <typename T> void do_something(T arg) override { // do something } }; Выглядит привычно, пробуем скомпилировать.... И оно не билдится. С++ не разрешает вам иметь виртуальный шаблонный метод. Но почему? Надо понимать, как под капотом работают виртуальные и шаблонные функции. При определении шаблона, никакой низкоуровневый код не генерируется. Код генерируется только, когда компилятор видит использование шаблона с каким-то конкретным типом. Подробнее можно тут почитать. Так вот реальный код инстанциаций шаблона может быть раскидан по очень большому множеству единиц трансляции. Теперь про виртуальные функции. В абсолютном большинстве компиляторов они реализованы через vtable. Подробнее тут. Но вкратце: во время компиляции класса с виртуальным формируется статический массив, состоящий из адресов скомпилированных виртуальных методов. Порядок нахождения в массиве определяется порядком объявления методов в классе. Почему вообще возможна такая генерация? Потому что компилятор уже в этой конкретной единице трансляции знает весь набор виртуальных методов в базовом классе и в наследнике. Поэтому он заранее может выбрать правильный размер vtable. Теперь пытаемся совместить. Для генерации vtable на этапе компиляции нам нужно знать весь набор виртульных методов. И это прямо противоречит тому факту, что шаблон ничего не знает о своих потенциальных инстанциациях, которые разбросаны по куче единиц трансляции. Каждая единица трансляции компилируется независимо, поэтому получить информацию об адресах конкретных инстанциаций из других TU невоз… Читать полностью →
17 · 2.5K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​std::source_location #новичкам Если вы до сих пор не знали, как платформонезависимо(безо всяких PRETTY_FUNCTION) получать человеческий доступ к имени файла, имени функции и строчке кода, где вы сейчас находитесь, то пост для вас. В С++20 появился класс std::source_location, который инкапсулирует в себе информацию о точке исходного кода, где был создан соответствующий объект. Применение до боли простое. У класса есть набор методов, которые в сумме и дают понимание о том, где был создан объект. Вы просто создаете объект через единственный статический метод std::source_location::current и получаете, что нужно. Пример: #include <iostream> #include <source_location> #include <string_view> void log(const std::string_view message, const std::source_location location = std::source_location::current()) { std::clog << "file: " << location.file_name() << '(' << location.line() << ':' << location.column() << ") `" << location.function_name() << "`: " << message << '\n'; } int main() { log("Hello world!"); } В момент вызова функции log компилятор должен вычислить все параметры, переданные в функцию. Поэтому параметр location с дефолтным параметром std::source_location::current() как раз и делает нужный трюк - location неявно присваивается расположение вызова функции в коде всякий раз, когда кто-то ее дергает. После чего нужно сериализовать location через вызовы методов в нужном порядке и вуаля, красивый лог: file: /app/example.cpp(17:8) int main(): Hello world! В больших фреймворках, типа userver вам конечно же не нужно о таком беспокоиться. Но если вы пилите что-то свое кастомное, то std::source_location - хорошая тула для дебаггинга и логирования. Define your place. Stay cool. #cpp20
19 · 2.1K ·
Грокаем C++
Агенты добрались до продакшена Про AI-агентов сейчас не говорит только ленивый. Обычно это выглядит так: «мы прикрутили LLM, дали ей пару тулов, теперь она сама все делает». А потом это пытаются выкатить в прод, и внезапно оказывается, что агент — это не чатик с промптом, а распределенная система с дорогущими GPU, долгими запросами, динамическим графом исполнения, ретраями, которые нельзя просто так делать, и метриками качества, которые надо в принципе уметь измерять и делать это каждый день. На недавнем deep tech night прошел показательный доклад «От поиска к агентам: путь Алисы», который я глянул. Круто, когда говорят не про то, как ллм и агенты делают космолеты, а какие вызовы несет в себе их использование и как сделать работу с языковыми моделями надежной, отслеживаемой и универсальной в рамках большой компании. Изыскания ребят привели их к AGTS - агентской транспортной системе. Если коротко, это сервис-медиатор между агентами, тулами и моделями. Все общаются не напрямую друг с другом, а через AGTS. И вот тут начинается нормальный backend: 🔸 агенты и тулы могут быть написаны на разных языках — Python, C++, Java/Kotlin, Go; 🔸 можно переиспользовать одну инфраструктуру для прода и приемки качества; 🔸 есть «железные пользователи» — агенты, которые имитируют пользовательское поведение; 🔸 LLM-as-a-judge - модели оценки качества "услуг агентов" - тоже живут как агенты на той же платформе; 🔸 AGTS кэширует запросы и ответы к моделям и тулам, чтобы при сбое не перезапускать всю дорогую LLM-мясорубку с нуля. Последний пункт особенно важен. Агент упал не в самом начале, а где-нибудь на пятой минуте размышлений? Не надо снова жечь GPU и молиться. Система делает fast-forward по закэшированным данным до точки падения и продолжает оттуда. Пожалуй один из главных поинтов доклада: harness > model. То есть качество можно серьезно растить не только заменой модели на новую большую и дорогую, а обвязкой вокруг нее. И эти обвязки централизовано добавляются и управляютс… Читать полностью →
20 · 2.4K ·
Г
Ссылка
нажмите — покажем
​​Identity объекта #опытным На пространстве интернетов мне попался шортс, где довольно опытный в С++ чувак рассказывает про некий "трюк" в контексте такой задачи: По сути, нужно построить центральный сервис, который отправляет обновления цен на станции, но новые желаемые цены могут поступать, пока старые обновления ещё в пути. Вас волнует только то, чтобы каждая станция в конечном итоге получила самую последнюю желаемую цену, при этом подтверждения (acknowledgements) должны приходить по порядку, а лишние обновления следует избегать. Таким образом, обновления, которые вы отправляете, могут прибывать на станцию не по порядку, но подтверждения, которые вы получаете обратно, всегда должны быть правильно упорядочены в том порядке, в котором станция применила обновление цены. И, по сути, спроектировать класс так, чтобы минимизировать время и тд. трюк в том, что если вам нужен идентификатор объекта, то вы можете использовать его уникальный адрес. Приводился такой пример: struct StationClient { [[nodiscard]] uintptr_t ClientId() const { return reinterpret_cast<uintptr_t>(this); } }; Выглядит с первого взгляда круто. Действительно, у каждого объекта свой уникальный адрес. Два объекта в программе не могут занимать одну ячейку памяти. Получается, что адрес можно использовать как id и это "просто работает". Только вот сходу можно назвать кучу проблем, к которому ведет этот подход: 🔞 id объекта меняется, если он перемещается 🔞 id объекта меняется, если он копируется(должны ли такие объекты копироваться и перемещать вообще - это вопрос, но проблемы остаются) 🔞 такой айди тесно связан с временем жизни объекта и не существует о отрыве объекта в программе 🔞 id переиспользуется другими объектами и на стеке, и на куче. Как только разрушился объект, новый объект может получить тот же самый id. И как различить объекты в таком случае непонятно. 🔞 id нельзя безопасно использовать как внешний идентификатор, аля передавать в другие сервисы или сохранять в хранили… Читать полностью →
14 · 2.4K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​std::spanstream #опытным Радостная весть для всех, кто пользуется iostreams! В C++23 добавлены новые классы, позволяющие работать с массивами символов как c потоками, без лишних копирований и динамических выделений памяти. Пусть у нас есть какой-то сырой буфер данных и мы хотим безопасно считать его с удобным и привычным iostream'овским апи, при этом не делая никаких копий. Для этого ввели std::ispanstream. Он принимает С++20 std::span в качестве буфера и сам не производит никаких аллокаций и копирований: char input[] = "10 20 30"; std::ispanstream is{span<char>{input}}; int i; is >> i; ASSERT_EQUAL(10,i); is >> i; ASSERT_EQUAL(20,i); is >> i; ASSERT_EQUAL(30,i); is >> i; std::ispanstream может работать с любыми символами, расположенными последовательно в памяти (другими словами, со всем, от чего можно сконструировать std::span). Для чтения данных фиксированного размера std::ispanstream фактически заменяет std::istringstream, потому что гарантирует отсутствие аллокаций. В общем, если вы как-то получили доступ к непрерывному сырому участку памяти и вам нужен удобный доступ к парсингу - std::ispanstream вам подойдет. Можно даже парсить данные из файла, если использовать mmap и замаппить данные файла в виртуальное пространство процесса. Тогда можно получить std::span на данные этого файла и создать std::ispanstream, парсящий их. Правда, если вы уже используете mmap, то скорее всего хотите какое-то производительное решение. А парсер стандартных потоков ввода-вывода мягко говоря не самый быстрый. Тот же std::from_chars будет быстрее и не менее стандартным. Don't allocate. Stay cool. #cpp23
22 · 2.2K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Stacktrace #опытным Одна из проблема исключений - непонятно, откуда оно прилетело. Ну да, по сообщению об ошибке и типу исключения обычно можно отследить нужное место в программе. Но как исполнение дошло до этой точки? А это ведь самое важное - понимание, при каком сценарии наступила исключительная ситуация. Когда мы дебажим программу, мы видим, что привело к точке останова. В любом дебаггере есть команда аля backtrace, которая выводит визуализированный стек вызовов. Например так: (gdb) bt #0 0x0000555555556a2a in apply_discount (order_id=1001, total=250.5) at discount.c:45 #1 0x0000555555556b10 in calculate_final_price (order=0x7fffffffde40) at order_engine.c:89 #2 0x0000555555556c45 in process_payment (order=0x7fffffffde40, method=0x555555758020 "credit") at payment.c:120 #3 0x0000555555556d8a in handle_order_request (request=0x7fffffffdf00) at api_handler.c:56 #4 0x0000555555556e20 in main () at server.c:32 В данном случае мы видим, что ошибка произошла в функции apply_discount, которую вызвал калькулятор цены при обработке запроса платежа. То есть в данном случае мы видим четкий сценарий возникновения ошибки И вот было бы круто видеть такую картину каждый раз, когда в catch залетает исключение. Да и в принципе в любой ситуации, когда нам интересна информация из трейса. С++23 предоставил нам возможность конструировать кастомные исключения с описанием стека вызова внутри. Делается это через класс std::stacktrace. Работает он очень просто: #include <iostream> #include <stacktrace> std::stacktrace bar() { auto trace = std::stacktrace::current(); return trace; } std::stacktrace foo() { return bar(); } int main() { auto trace = foo(); std::cout << trace << '\n'; } С помощью статического метода std::stacktrace::current() можно получить стек вызовов в моменте использования метода. Для типа std::stacktrace даже перегружен оператор вывода в поток, поэтому вы спокойно можете вывести сырой трейс на консоль. Вывод может быть примерно т… Читать полностью →
22 · 1.6K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Stacktrace. Tips #опытным Чтобы полноценно работать со стандартными трейсами, нужно знать несколько вещей: ✅ Чтобы фича заработала с новыми версиями gcc и clang, вам нужно залинковать либу -lstdc++exp. В туториалах обычно используется линковка с -lstdc++_libbacktrace, которая не работает на свежих версиях компиляторов ✅ Собирайте код с дебаг символами. Если хотите видеть трейсы и получать из них полезную информацию об именах функций и файлов, вместо прочтения чистых адресов, то надо собирать в дебаге. Итого полная команда сборки: g++ -std=c++23 -g -lstdc++exp -O2 main.cpp -o app ✅ Да, вы можете включать оптимизации компилятора и добавить в бинарник дебаг символы одновременно. Оптимизации конечно могу сильно преобразовывать потенциальные трейсы за счет инлайнинга, но без оптимизаций мы не можем. ✅ Дебаг символы обычно занимают довольно внушительный объем и не всегда хочется их тянуть везде вместе с бинарем. В таких случаях можно использовать утилиту strip и вырезать из бинаря отдельный файл с дебаг символами. При обнаружении проблемы с программой можно в дебаггере подгрузить файл с символами и наслаждаться прелестями человекочитаемой отладки. Ну или можно собирать код с флагом -g1, который включает только самую необходимую отладочную информацию ✅ Мы рассматривали стектрейсы в разрезе исключений, но их конечно же можно использовать и отдельно от них. int foo(int a, int b) { if (b == 0) { LOG_ERROR << "Second operand is zero. Trace:\n" << std::stacktrace::current(); } return a / b; } Однако стоит помнить, что получение трейса - не бесплатная штука. Логи-то не бесплатный, а проход по стеку вызовов и сбор информации из дебаг символов - очень затратная штука. Не стоит использовать трейсы в нагруженных частях программы. Только в ошибочных путях программы в некритичном коде. ✅ Сам класс стектрейсов предоставляет много гибкости. Можно отдельно проходиться по каждому фрейму и в принципе контролировать количество захватываемых фреймов: void i… Читать полностью →
18 · 1.1K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
Откуда spurious wakeup на кондваре? #опытным У кондваров есть метод std::condition_variable::wait, который по документации блокирует исполнение потока до тех пор, пока не придет уведомление или не произойдет тн spurious wakeup(ложное пробуждение). Поэтому нам говорят, что нужно ставить wait в цикл while: while (!pred()) cv.wait(lock); или использовать перегрузку, передавая в кондвар предикат: cv.wait(lock, pred); Но что это за такие ложные пробуждения, из-за которых на нужно крутиться в цикле? По сути, ложными пробуждениями можно назвать любую ситуацию, когда поток просыпается и условие предиката неудовлетворяется. Со стороны кажется, что поток просто в рандомный момент сам проснулся, когда условия еще не создались для него. Но это не совсем так. Давайте покопаемся в причинах. 👉🏿 Поток может проснуться без уведомления. Звучит как баг или признаки рождения скайнета, но все прозаичнее. На реальных системах есть свои реальные причины такого поведения. В линуксе раньше для управления потоками(запуск, остановка и тд) система использовала сигналы. В такой модели условная переменная физически могла быть разбужена сигналом, предназначенным для совершенно другой цели — например, менеджер потоков решил передать потоку команду, а сигнал попал в тот же обработчик, что и пробуждение от cond_signal. POSIX стандарт должен был учитывать эту особенность, поэтому прям в доке прописал возможность пробуждения без сигнала. Свежих версиях линукса такой ситуации уже не может быть и из pthread'ной функции ожидания cv управление не вернется в юзерспейс при отправке любых сигналов. Но наследие осталось. 👉🏿 А вот тут уже реальная проблема с race condition. Поток реально получил сигнал на пробуждение, проснулся, начал выполняться и увидел, что условие ложно. Потому что по сути какой-то другой поток уже успел обработать появившееся событие. Это может произойти по многим причинам, но может быть вот такая ситуация: 1. Поток A спит на cond_wait. 2. Поток B меняет условие и выз… Читать полностью →
11 · 955 ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Где-то баг... #опытным Вот вам код: struct Order { int price; int id; }; int main() { std::vector<Order> book; for (int i = 0; i < 40; ++i) { book.push_back({100, i}); } std::sort(book.begin(), book.end(), [](const Order& a, const Order& b) { return a.price <= b.price; }); std::cout << "book.size() = " << book.size() << '\n'; return 0; } Все ли в нем хорошо? Вы уверены? В чем здесь могут быть проблемы и какие у них проявления? Напишите в комментах. Prevent bugs. Stay cool.
9 · 1.4K ·
Грокаем C++
Фотография
Немного поиграл с настройками и собрал город в духе La Belle Époque: с исторической застройкой, насыщенными улицами и минимумом футуризма 🚂 Это не свежая часть SimCity, а новая игра от YADRO, где смешались архитектура прошлого, современные технологии и массовая культура — от здания Наркомфина до «Бегущего по лезвию». А какой город получится у вас? Выкрутите память на максимум — и построите известные шедевры прошлого 🏛 Или выберете летающие автомобили и технологии будущего 🚄 Вам решать! И не забудьте поддержать YADRO в рейтинге работодателей hh 😎
4 · 551 ·
Г
Ссылка
нажмите — покажем
​​Strict weak ordering #опытным На первый взгляд, всё выглядит рабочим: мы создаём 40 заказов с одинаковой ценой 100 и уникальными id, затем сортируем по возрастанию цены, используя <=. Программа компилируется и даже выполняется без ошибок… пока не достигнет определённого размера. Тогда она падает с segmentation fault. struct Order { int price; int id; }; int main() { std::vector<Order> book; for (int i = 0; i < 40; ++i) { book.push_back({100, i}); } std::sort(book.begin(), book.end(), [](const Order& a, const Order& b) { return a.price <= b.price; }); std::cout << "book.size() = " << book.size() << '\n'; return 0; } Но почему так? Мы же вообще ничего криминального не делали, а получили UB. Баг на самом деле в том, что компаратор не удовлетворяет требованию strict weak ordering. Конкретно проблема в иррефлексивности компаратора. Для любых двух элементов с одинаковой ценой (в нашем случае все цены равны 100) компаратор возвращает true в обе стороны: - comp(a, b) = 100 <= 100 → true - comp(b, a) = 100 <= 100 → true Это означает, что comp(a,a) тоже true (так как 100 <= 100). Согласно стандарту, компаратор должен возвращать false для сравнения объекта с самим собой. Это и есть требование иррефлексивности. Нарушение этого правила приводит к тому, что алгоритм сортировки некорректно сравнивает одинаковые элементы и по итогу входит за границы последовательности Ну окей. "А почему эффекты проявляются только после определенного размера последовательности?" Потому что реализация std::sort в стандартной библиотеке обычно использует интроспективную сортировку (introsort), которая выбирает стратегию в зависимости от размера диапазона: 1️⃣ Для малых подмассивов (обычно < 16 элементов) используется сортировка вставками (insertion sort). На каждом шаге работы эта сортировка проходится по всем предыдущим элементам и меняет текущий, если компаратор возвращает true. Есть четкая гран… Читать полностью →
15 · 2K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​WAT #новичкам Как вы думаете, результат выражения: bool math = -1 < 1U; будет правдой или ложью? Нормальный человек думает, что математика работает. Но С++ выкидывает ее в мусор и по итогу math == false. #ЧЗХ ? Вместо того, чтобы приводить беззнаковое число к знаковому, С++ делает наоборот. В итоге -1 должен конвертироваться в неотрицательное число. В дополненном коде -1 это 0xFFFFFFFF ака все единички, поэтому при касте к беззнаковому числу оно просто становится UINT32_MAX. И оно конечно больше 1. Выход тут один. Не сравнивать знаковые и беззнаковые числа в одном выражении. Включайте -Wall -Werror и получите ошибку компиляции. После исправления арифметика будет работать как ожидается. Mix things consciously. Stay cool #cppcore
9 · 1.4K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Что внутри std::condition_variable? #опытным Более менее все знакомы с примитивом синхронизации "условная переменная". При наличии мьютекса она может погрузить поток в сон до тех пор, пока не поступит уведомлении и условие пробуждение не будет истинным. Сильно упрощая, в консюмере ждем события на wait, а в продюсере уведомляем через notify о возникновении события. std::mutex mtx; std::condition_variable cv; bool ready = false; void consumer() { std::unique_lock lock(mtx); cv.wait(lock, []{ return ready; }); // Wait until 'ready' is true // Proceed with work } void producer() { { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // Wake up one waiting thread } Но как это работает под катопом? Если задуматься, то кондвар очень похож на семафор и мьютекс. По сути оба отправляют поток спать, когда счетчик пытаются декрементить ниже 0. И они оба уведомляют одного из ожидающих, когда счетчик становится равным 1 или больше. Да, семантика разная, но факт на лицо: есть и ожидание и уведомление. Так может у кондваров и мьютексов/семафоров есть общие инструменты реализации? Бинго! На линуксе есть такая штука - futex. Fast Userspace muTEX. Это низкоуровневый примитив синхронизации в ядре. Главная идея: быстрый путь (захват/освобождение без конкуренции) делается в userspace обычными атомарными операциями. Ядро вызывается только когда нужно усыпить или разбудить поток. Если надо усыпить поток - он помещается в очередь ожидания. Если надо разбудить поток - он достается из головы очереди и продолжает исполнение. Так вот кондвары и реализованы на основе такой очереди. Конечно, нужно довольно много всего построить вокруг помещения потока в ожидание, но принцип такой. При вызове wait поток освобождает переданный мьютекс и засыпает(вызывается условный futex_wait), помещаясь в очередь ожидания. При уведомлении (вызов futex_wake) о наступлении условия поток достается из очереди, буди… Читать полностью →
❤12🔥8👍6
19 · 1.8K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
Оптимистичные и пессимистичные блокировки #опытным Концепция блокировок уходит далеко за пределы языковых примитивов синхронизации. Блокировки актуальны в принципе для любых систем, с которыми можно работать конкурентно. И идейно для таких систем существует 2 типа локов: оптимистичные и пессимистичные. Они имеют принципиальные отличия и это важно понимать для того, чтобы корректно оперировать данными и их обновлениями. Разница между ними в том, как предотвращается конфликт изменения данных из-за конкурентного доступа и что происходит с потоком/транзакцией до этого момента. Пусть для примера у нас будет какая-то ассоциативная структура с кучей записей. И приходит очередной запрос на изменение одной конкретной записи. Начнем с простого. Пессимистичная блокировка. Ее используют, когда есть риск конкурентного доступа/изменения данных и мы не хотим принимать этот риск. Например, в системах, где очень важна консистентность данных - банковский сектор. При пессимистичной блокировке мы запрашиваем эксклюзивный доступ к конкретной записи и при получении доступа больше никто не может изменять ее одновременно с нами. Реализацией пессимистичной блокировки в С++ является всем известный мьютекс. Он обеспечивает эксклюзивный доступ к данным: struct Record { int value = 0; std::mutex mtx; }; void update_record(Record& rec, int new_value) { std::lock_guard<std::mutex> lock(rec.mtx); rec.value = new_value; } Поток, который хочет изменить запись, сначала захватывает мьютекс. Если мьютекс уже кем-то занят, поток встаёт в очередь ожидания и спит, пока владелец не освободит его. В этот момент никто другой не может изменить rec.value — мы работаем в условиях полной изоляции. Плюсами являются предсказуемость, отсутствие откатов, гарантированная консистентность данных. Но за это приходится платить: блокировка и ожидание других запросов, возможность дедлоков, никакого параллелизма и оверхэд на сам факт блокировки записи(например системные вызовы и тд). Теперь предс… Читать полностью →
👍17❤6🔥5
14 · 1.1K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​std::atomic::wait под капотом #опытным В C++20 у атомиков появились wait и notify_one/all. void atomic::wait( T old, std::memory_order order = std::memory_order_seq_cst ) const noexcept; void atomic::notify_one() noexcept; void atomic::notify_all() noexcept; Обычно атомики - это про спинлоки и горячие циклы, а тут какой-то интерфейс аля кондвар. В доке написано, что метод wait блокируется , пока не изменится значение атомика с old на новое. Но как это реализовано? Сегодня будем разбираться. Будем смотреть на реализацию внутри линукса, на других платформах принципы схожи. Для начала: нет, метод wait - это не спинлок. Спинлок - хороший вариант, когда ситуация меняется очень быстро. Но он будет проедать ресурсы вхолостую, если долго не будет нотификации. Поэтому в общем случае нам необходимо засыпать и делать дорогой сискол, чтобы не жечь проц. Но немного спинлока перед сном нам все равно не помешает. И что же за такой сискол, который относительно быстро блокает тред до нотификации? Конечно, наш уже знакомый и любимый futex. Именно этот примитив - основа для "атомарного" ожидания. Реализация определяет примерно такую функцию ожидания и нотификации: template<typename _Tp> __platform_wait(const _Tp* __addr, __platform_wait_t __val) noexcept { auto __e = syscall (SYS_futex, static_cast<const void*>(__addr), static_cast<int>(__futex_wait_flags::__wait_private), __val, nullptr); if (!__e || errno == EAGAIN) return; if (errno != EINTR) __throw_system_error(errno); } template<typename _Tp> void __platform_notify(const _Tp* __addr, bool __all) noexcept { syscall (SYS_futex, static_cast<const void*>(__addr), static_cast<int>(__futex_wait_flags::__wake_private), __all ? INT_MAX : 1); } Если мы поверх фьютекса накрутим несколько раундов проверки в горячем цикле, то вуаля, получили std::atomic::wait Но как всегда есть нюансы. 1️⃣ Фьютекс ассоци… Читать полностью →
21 · 3.1K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Какая память быстрее: стек или куча? #новичкам Это довольно общий и не совсем корректный вопрос, но он специально так поставлен, что ответ был чуть более развернутый. Все равно примерно все понимают его смысл. Работа с памятью начинается с ее выделения. Вот у нас ничего не было и мы захотели создать объект на стеке и в куче. Что будет быстрее? int main() { std::vector<int> vec; std::unique_ptr<std::vector<int>> p_vec = new std::vector<int>{}; return 0; } Тут ответ более менее однозначный. Выделение/освобождение памяти происходит быстрее на стеке. Аллокация на стеке - это просто сдвиг указателя стека на какое-то количество байт. Аллокация на куче - это поиск блока нужного размера во внутренней структуре аллокатора и, потенциально, системный вызов, если блока не нашлось. Короче, ощутимо дольше. Но! Стековая память и память кучи ничем принципиально друг от друга не отличаются. Это все та же RAM. Имея готовые куски памяти в обоих регионах, вы можете работать с ними с одинаковой скоростью чтения/записи. Стек просто заранее предвыделяется при создании потока/процесса. Он уже готовенький для использования и изолирован. Но память, которую мы получаем через new, может быть доступна другим потокам/процессам, поэтому ее надо запрашивать в общем случае через ОС, чтобы нечаянно(или чаянно) чего не натворить. В общем-то говоря, похожим образом работают быстрые кастомные аллокаторы - они заранее выделяют большую арену, которую потом очень эффективно можно использовать для размещения объектов. Be faster. Stay cool. #base
19 · 3K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Кондвар - это не брокер сообщений #новичкам Думаю, некоторые из вас работали с кафкой и наслаждались ее функционалом. Этот инструмент позволяет в потоке передавать события от продюсера к консюмеру, при этом сохраняя эти события на случай сбоев, необходимости перечитывания или асинхронной работы продюсера и консюмера. Прекрасная технология. И кондвар не реализует практически ничего из этого. Главная ошибка в рассуждениях - Producer: notify_one(); Consumer: wait(); думать, что notify_one() — это сигнал, который где-то лежит и ждёт, пока потребитель вызовет wait(). Если нотификация произошла до вызова wait, то она безвозвратно потеряется. Для многих это очевидно, но этот факт смещает акценты при проектировании протокола синхронизации. Условно: пусть рядом с вашей дверью находится ваш почтовый ящик. Вы можете каждый раз, когда приходите домой проверять ящик на наличие новых писем. Но скорее всего там очень часто будет пусто. Вместо этого вы говорите почтальону: "вот у меня есть дверной звонок, звоните в него каждый раз, когда приносите письма". И это работает, когда вы дома. Но вы пропустите уведомление, если вас не будет. И по приходу никак не поймете, что приходил почтальон и звонил в звонок. Вместо простого звонка, нужно менять состояние в квартире - зажигать лампочку при звонке например. Так вы зайдете в квартиру, увидите лампочку и пойдете проверять почтовый ящик. То есть основа синхронизации тут не звонок, а лампочка. Звонок нужен, чтобы вы отреагировали, когда вы дома и ждете писем. Так и в программе. Точка синхронизации - это разделяемое состояние, которое изменяет продюсер и читает консюмер. Сам кондвар - это средство предотвращения постоянной проверки. Он не несет в себе никакой другой полезной информации, кроме "что-то произошло, но возможно и нет". Вся полезная информация - в разделяемом состоянии. Поэтому нотифай должен восприниматься: 🔍 Продюсером как клич в пустоту, что разделяемое состояние поменялось. Если никто не услышит, ну и ладн… Читать полностью →
18 · 2.7K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​Напишите в комментариях самые запоминающиеся/интересные/сложные вопросы/задачи по С++ с интервью, которые вы проходили. Надеюсь из поста читатели смогут для себя подчерпнуть интересные моменты и найти точки роста, чтобы в следующий раз точно пройти собес. Я начну. Написать потокобезопасную очередь с приоритетом. Приоритета всего 3. Количество тасок с 1 и 2 приоритетом в очереди ограничено, с наименьшим приоритетом - неограничено. При попытке вставить в очередь слишком много задач 1 или 2 приоритета, этот метод блочится. Доставаться из очереди таски должны строго по убыванию приоритета: только при отсутствии задач более высокого приоритета можно начать отдавать задачи более низкого. Practice interview. Stay cool.
18 · 2.5K ·
Грокаем C++
Фотография
Экскурсия по лабораториям YADRO: как тестируют серверы и телеком-оборудование В ноябре и декабре YADRO проводит экскурсии по своим лабораториям в Санкт-Петербурге и Москве. На маршрутах — тестовые стенды, оборудование на разных этапах отладки и рабочие пространства команд. Инженеры расскажут, какие проверки проходит железо, и ответят на вопросы. 📍Санкт-Петербург — 12 ноября и 1 декабря 📍Москва — 17 ноября и 8 декабря Сбор гостей в 18:30, старт в 19:00. Экскурсия занимает до двух часов. Участие бесплатное — нужно зарегистрироваться и дождаться подтверждения от организаторов. Возраст участников — от 18 лет. Зарегистрироваться Реклама. ООО "КНС ГРУПП". ИНН 7701411241. erid: 2W5zFGVnpQA
28 · 1.8K ·
Г
Ссылка
нажмите — покажем
​​С++26. Или все-таки 27? Немного с опозданием, но новость все-таки на канале. Черновик С++26 не удалось утвердить с первого раза на этапе голосования национальных органов ISO. Фактически, принятие С++26 отложено на неопределенный срок. Почему так? Причиной скорее всего являются контракты. По сути, это возможность писать что-то вроде int divide(int a, int b) pre(b != 0) { ... }, где pre — это контракт, проверяемый во время выполнения или компиляции. В общем, это какие-то пред и/или постусловия, которым должны удовлетворять функции. Вроде идея-то классная. Но ее дизайн в стандарте оказался не очень удачным. Против него высказалось достаточно тяжеловесов мира плюсов, в том числе и сам Страуструп. По поводу контрактов были ожесточенные дискуссии и в драфте С++26 был скорее некий MVP контрактов, нежели полноценная функциональность. Бьерн же сказал, что «это не минимально жизнеспособный продукт. Это не минимально и не жизнеспособно», и что контракты добавляют сложность «очень неясными способами», меняя смысл кода в зависимости от контекста. Если кому очень интересно, то вот и вот две бумаги от Бьерна и его коллег, критикующих контракты. Там критика и по функционалу(очень сложно, описание примерно в 56 страниц текста), и по перформансу(не zero-cost) и по процедуре внедрения в драфт стандарта. Тем не менее Страуструп и соавторы не против идеи контрактов как таковой. Они против конкретного неудачного дизайна из черновика С++26. В качестве альтернативы они предлагают: 👉🏿 Признать контракты просто отладочным инструментом, а не функцией безопасности. 👉🏿 Использовать гораздо более простой и мощный библиотечный дизайн, который уже реализован, протестирован и совместим (ссылаются на D4324R0 / P4324R0 — "C++ Design By Contract: Minimum Language, Maximum Library"). 👉🏿 Вместо огромного и неполного языкового механизма сделать ставку на hardened standard libraries (ужесточенные стандартные библиотеки) с минимальными накладными расходами (замерено ~0.3% в production у G… Читать полностью →
⚡72❤7👍5🗿2🤔1
24 · 1.5K ·
Г
Грокаем C++
Ссылка
нажмите — покажем
​​LRU-кэш на собеседовании #опытным LRU (Least Recently Used) - классический вопрос на собеседованиях. Вроде не чистый литкоды, можно и ООП пописать, и помикроархитектурить. Но тем не менее задача проверяет знание алгоритмов и структур данных применительно к реально используемой штуке. Требования к задаче: 👉🏿 Реализовать LRU-кэш с фиксированной ёмкостью, задаваемой в конструкторе. 👉🏿 Операции get(key) и put(key, value) должны работать за O(1) в среднем. 👉🏿 При переполнении удаляется самый редкоиспользованный элемент. 👉🏿 get возвращает ссылку на значение значение, если ключ есть, иначе - сигнал об отсутствии. 👉🏿 нет требований на потокобезопасность. Типичное решение, безо всяких ухищрений: двусвязный список (std::list) для порядка использования + хеш-таблица (std::unordered_map) для доступа по ключу. template<typename Key, typename Value> class LRUCache { public: using value_type = std::pair<Key, Value>; using list_type = std::list<value_type>; using list_iterator = typename list_type::iterator; using map_type = std::unordered_map<Key, list_iterator>; explicit LRUCache(size_t capacity) : cap_(capacity) { if (cap_ == 0) throw std::invalid_argument("Capacity must be > 0"); } std::optional<std::reference_wrapper<const Value>> get(const Key& key) { auto it = idx_.find(key); if (it == idx_.end()) return std::nullopt; entries_.splice(entries_.begin(), entries_, it->second); return std::cref(it->second->second); } void put(const Key& key, Value value) { auto it = idx_.find(key); if (it != idx_.end()) { it->second->second = std::move(value); entries_.splice(entries_.begin(), entries_, it->second); return; } if (entries_.size() == cap_) { const Key& k_evict = entries_.back().first; idx_.erase(k_evict); entries_.pop_back(); } entries_.emplace_front(key, std::move(value)); … Читать полностью →
👍16❤10🔥6🤓1⚡1
46 · 1.5K ·

Открытая публичная лента из поискового индекса ChatCrawler — «Google по публичному Telegram»; обновляется по мере обхода площадки. Время — UTC.

Только публичный контент, официальный API Telegram. О проекте · Вопросы · Границы индекса · Убрать страницу из выдачи · Каталог · Поиск · Как мы считаем