es
Feedback

¡No caigas en manos de tramposos! Telemetrio encuentra y marca estos canales 👉 Si quieres ver la etiqueta, suscríbete

Грокаем C++

Грокаем C++

Ir al canal en Telegram

Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

Mostrar más
9 351
Suscriptores
-424 horas
+57 días
-830 días
Atraer Suscriptores
oct '26
octubre '26
+121
en 0 canales
septiembre '26
+127
en 1 canales
Get PRO
agosto '26
+113
en 0 canales
Get PRO
julio '26
+79
en 0 canales
Get PRO
junio '26
+111
en 0 canales
Get PRO
mayo '26
+153
en 0 canales
Get PRO
abril '26
+149
en 0 canales
Get PRO
marzo '26
+95
en 0 canales
Get PRO
febrero '26
+126
en 1 canales
Get PRO
enero '26
+128
en 0 canales
Get PRO
diciembre '25
+143
en 0 canales
Get PRO
noviembre '25
+163
en 0 canales
Get PRO
octubre '25
+113
en 1 canales
Get PRO
septiembre '25
+119
en 1 canales
Get PRO
agosto '25
+228
en 1 canales
Get PRO
julio '25
+578
en 4 canales
Get PRO
junio '25
+724
en 2 canales
Get PRO
mayo '25
+307
en 2 canales
Get PRO
abril '25
+148
en 1 canales
Get PRO
marzo '25
+246
en 1 canales
Get PRO
febrero '25
+487
en 6 canales
Get PRO
enero '25
+233
en 1 canales
Get PRO
diciembre '24
+293
en 0 canales
Get PRO
noviembre '24
+215
en 0 canales
Get PRO
octubre '24
+1 593
en 18 canales
Get PRO
septiembre '24
+629
en 5 canales
Get PRO
agosto '24
+490
en 5 canales
Get PRO
julio '24
+206
en 2 canales
Get PRO
junio '24
+1 383
en 19 canales
Get PRO
mayo '24
+833
en 7 canales
Get PRO
abril '24
+1 851
en 11 canales
Get PRO
marzo '24
+350
en 0 canales
Get PRO
febrero '24
+691
en 0 canales
Fecha
Crecimiento de Suscriptores
Menciones
Canales
26 octubre+6
25 octubre+1
24 octubre+4
23 octubre+2
22 octubre+6
21 octubre+7
20 octubre+7
19 octubre+4
18 octubre+9
17 octubre+2
16 octubre+3
15 octubre+3
14 octubre+4
13 octubre+8
12 octubre+6
11 octubre+4
10 octubre+7
09 octubre+7
08 octubre+1
07 octubre+7
06 octubre+3
05 octubre+2
04 octubre+7
03 octubre+4
02 octubre+2
01 octubre+5
Publicaciones del Canal
​​Какая память быстрее: стек или куча? #новичкам Это довольно общий и не совсем корректный вопрос, но он специально так поставлен, что ответ был чуть более развернутый. Все равно примерно все понимают его смысл. Работа с памятью начинается с ее выделения. Вот у нас ничего не было и мы захотели создать объект на стеке и в куче. Что будет быстрее?
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

2
Доступные по цене AI-подписки, да ещё и с гарантией на весь срок? В AI Market это норма 🙃 Пользуешься сразу несколькими нейр
Доступные по цене AI-подписки, да ещё и с гарантией на весь срок? В AI Market это норма 🙃 Пользуешься сразу несколькими нейросетями, но оплата подписок быстро превращается в отдельную задачу? То карта не проходит, то продавец пропал, то цена вдруг другая. 🤖AI Market решает эти проблемы разом. Магазин подключает официальные AI-подписки прямо на ВАШ личный аккаунт — никаких общих и «левых» аккаунтов. ☺️ Гарантия не на «первые сутки, а потом как повезёт» — на весь оплаченный срок: восстановят, заменят или вернут деньги. 💡 В магазине есть подписки на ChatGPT, Claude, Gemini, Grok, Midjourney 💳 Оплата российской картой или через СБП ⏰ Активация на ваш личный аккаунт за 5–20 минут 🆓 Нет аккаунта? Создадим бесплатно ⭐️ Не первый год на рынке подписок, знаем все нюансы ⚙️ На связи живой менеджер, а не бот: поможем, если появятся вопросы. 👉 Выбрать подписку в AI Market
1 460
3
​​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️⃣ Фьютекс ассоциирован с 32-битной атомарной переменной. И конечно, такие атомики бывают, но не все они имеют такой размер. Ну то есть в кейсе с 32-битной целочисленной атомарной переменной можно внутри ядра навесить именно на нее функции "опорной переменной" для фьютекса. Но для атомиков другой размерности нужен другой подход. Можно было бы в каждый атомик класть 4 байта данных, с которыми будет ассоциироваться мьютекс. Но это будет слишком раздувать размер типа. Разработчики пошли другим путем: глобальная хэш-мапа отображения адресов атомарных переменных в данные фьютекса. 2️⃣ Есть же версии платформы, на которых нет futex'а. В этом случае в глобальной хэш мапе хранится мьютекс и кондвар и атомарное ожидание вырождается в ожидание на кондваре. Итак, алгоритм ожидания выглядит так: 1. Загружаем текущее значение атомика и сравниваем с old. Если уже не равно — выходим, ждать нечего. 2. Крутимся в спин-цикле фиксированное число итераций, на каждой проверяя значение. Если изменилось — выходим. Это спасает от дорогого сискола, когда ожидание короткое. 3. Если тип атомика — скаляр размером ровно с __platform_wait_t (32 бита на Linux) и с правильным выравниванием, то futex можно повесить прямо на адрес атомика: if constexpr (__platform_wait_uses_type<_Tp>) __platform_wait(__addr, __old); // futex_wait на самом атомике 4. Иначе — идём в глобальную хэш-таблицу __waiter_pool, которая отображает адрес атомика на один из 16 пулов. Внутри каждого пула есть своё 32-битное слово _M_ver, на котором и будет висеть futex: auto& __w = __waiter_pool::_S_for(__addr); __w._M_do_wait(&__w._M_ver, __w._M_ver); // futex_wait на версии пула 5. Если платформенного wait нет вообще (экзотические платформы), в том же пуле лежат mutex и condition_variable, и ожидание вырождается в классический cv.wait(lock, pred) на версии пула. Be efficient. Stay cool. #concurrency #cppcore
2 268
4
​​Оптимистичные и пессимистичные блокировки #опытным Концепция блокировок уходит далеко за пределы языковых примитивов синхронизации. Блокировки актуальны в принципе для любых систем, с которыми можно работать конкурентно. И идейно для таких систем существует 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 — мы работаем в условиях полной изоляции. Плюсами являются предсказуемость, отсутствие откатов, гарантированная консистентность данных. Но за это приходится платить: блокировка и ожидание других запросов, возможность дедлоков, никакого параллелизма и оверхэд на сам факт блокировки записи(например системные вызовы и тд). Теперь представим, что конфликты редки. Например, у нас миллиарды записей, и запросы на изменение приходят к разным записям. Блокировать каждую запись мьютексом — значит платить за блокировку там, где она почти никогда не нужна. Именно для таких случаев придумали оптимистичную блокировку. Идея проста: мы не блокируем запись. Мы читаем её, запоминаем версию (или timestamp), работаем, а перед записью проверяем — не изменил ли кто-то запись, пока мы думали. Если не изменил - записываем. Если изменил - откатываемся и повторяем. Для С++ нет отдельного примитива под это дело, потому что оптимистическая блокировка - это скорее паттерн, который надо отдельно внедрять в логику работы с данными. Но его легко собрать из std::atomic и CAS (compare-exchange): struct Record { // [ version: 32 бита ][ value: 32 бита ] std::atomic<uint64_t> state{0}; }; bool try_update(Record& rec, int new_value) { uint64_t old = rec.state.load(); uint32_t version = uint32_t(old >> 32); uint64_t desired = (uint64_t(version + 1) << 32) | uint32_t(new_value); return rec.state.compare_exchange_strong( old, desired); } void update_with_retry(Record& rec, int new_value) { while (!try_update(rec, new_value)) { } } Здесь state - это счётчик версии, который увеличивается при каждом успешном обновлении, + само значение value. Если между нашим чтением и попыткой записи кто-то другой уже обновил запись, первая часть state изменится, и compare_exchange_strong вернёт false. Тогда просто повторяем попытку. Плюсами здесь будет: отсутствие блокировок и накладных расходов на них, нет ожиданий, высокая параллельность при редких конфликтах. Минусы - при частых конфликтах много откатов и повторной работы, нагрузка на CPU. Ну и для некоторых доменов просто неприемлемо будет ретраить запросы и/или повторно пересчитывать актуальное значение. Выбор между двумя типами блокировок - это выбор между гарантией и параллелизмом. И хороший инженер должен уметь считать, что более важно в его конкретной задаче. Give guarantees. Stay cool. #concurrency
2 420
5
​​Что внутри 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) о наступлении условия поток достается из очереди, будится и конкурирует за захват мьютекса, чтобы честно проверить верность условия(может быть stolen wakeup и поток обратно уйдет спать). Прямым аналогом futex в Windows является семейство функций WaitOnAddress и WakeByAddressSingle/WakeByAddressAll. Don't busy wait. Stay cool. #concurrency #OS
2 581
6
​​WAT #новичкам Как вы думаете, результат выражения: bool math = -1 < 1U; будет правдой или ложью? Нормальный человек думает, что математика работает. Но С++ выкидывает ее в мусор и по итогу math == false. #ЧЗХ ? Вместо того, чтобы приводить беззнаковое число к знаковому, С++ делает наоборот. В итоге -1 должен конвертироваться в неотрицательное число. В дополненном коде -1 это 0xFFFFFFFF ака все единички, поэтому при касте к беззнаковому числу оно просто становится UINT32_MAX. И оно конечно больше 1. Выход тут один. Не сравнивать знаковые и беззнаковые числа в одном выражении. Включайте -Wall -Werror и получите ошибку компиляции. После исправления арифметика будет работать как ожидается. Mix things consciously. Stay cool #cppcore
2 921
7
​​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. Есть четкая граница остановки - первый элемент. Поэтому даже если компаратор будет возвращать true для одинаковых элементов, то это приведет лишь к лишним обменам элементов. 2️⃣ Для больших диапазонов introsort переключается на быструю сортировку (quicksort) с разбиением (partitioning) вокруг опорного элемента (pivot). Именно в фазе разбиения и прячется баг. Алгоритм разбиения тут примерно следующий. Обычно выбирается опорный элемент pivot. Затем два указателя двигаются навстречу друг другу, пока не найдут элементы, которые должны быть поменяны местами: while (comp(*left, pivot)) ++left; // пока левый < pivot while (comp(pivot, *right)) --right; // пока pivot < правый Если comp использует <=, то для равных элементов оба условия вернут true: - comp(*left, pivot) вернёт true (если *left == pivot, то pivot <= pivot → true) - comp(pivot, *right) тоже true (если *right == pivot). Таким образом, левый указатель будет двигаться вправо, пока не выйдет за границы, а правый — влево, также за пределы. Это приводит к чтению/записи за пределами массива и, в конечном счёте, к крашу. Решение тут простое и даже есть соответствующая мемная паста на кодфосес. "Компаратор в С++ должен возвращать false, если аргументы равны". Или в нашем примере: std::sort(book.begin(), book.end(), [](const Order& a, const Order& b) { return a.price < b.price; }); Reflect on your life. Stay cool. #STL
3 367
8
Немного поиграл с настройками и собрал город в духе La Belle Époque: с исторической застройкой, насыщенными улицами и минимум
Немного поиграл с настройками и собрал город в духе La Belle Époque: с исторической застройкой, насыщенными улицами и минимумом футуризма 🚂 Это не свежая часть SimCity, а новая игра от YADRO, где смешались архитектура прошлого, современные технологии и массовая культура — от здания Наркомфина до «Бегущего по лезвию». А какой город получится у вас? Выкрутите память на максимум — и построите известные шедевры прошлого 🏛 Или выберете летающие автомобили и технологии будущего 🚄 Вам решать! И не забудьте поддержать YADRO в рейтинге работодателей hh 😎
1 190
9
​​Где-то баг... #опытным Вот вам код: 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.
3 055
10
​​Откуда 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 меняет условие и вызывает notify_one. 3. Поток A просыпается, но ещё не запланирован шедулером. 4. Поток C успевает изменить условие обратно (забрал задачу из очереди, сбросил флаг). 5. Поток A наконец получает процессор, проверяет условие — оно false. Это называется stolen wakeup или такая 1. Поток A спит на cond_wait. 2. Поток B меняет условие и вызывает notify_one. 3. Поток A просыпается, но ещё не запланирован шедулером. 4. Поток А успевает изменить условие еще раз(или несколько). 5. Поток A наконец получает процессор, проверяет условие — оно false. То есть событие было фактически потеряно. Да, в вашем конкретном случае такого может и не произойти просто по логике программы. А можно не думать и просто использовать цикл с кондваром, хуже точно не будет. Спасибо, @mike_hd, за вопрос в чате и идею для поста. И всем комментаторам, вовлекшимся в обсуждение, за предоставление фактуры по вопросу. Don't get stolen. Stay cool. #concurrency
2 973
11
​​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 inspectTrace(const std::stacktrace& trace) { for (const auto& frame : trace) { std::cout << "Function : " << frame.description() << '\n' << "Source file : " << frame.source_file() << '\n' << "Line : " << frame.source_line() << '\n' << "---\n"; } } // Capture at most 10 frames except of the last one. auto trace = std::stacktrace::current(/skip=/1, /max_depth=/10); Трейсы скорее всего рано или поздно заедут в каком-то виде в стандарт, чтобы их можно было использовать со стандартными исключениями и конктейнерами. Поэтому полезно же сейчас понимать, как это работает под капотом и как этим правильно пользоваться. Know your origin. Stay cool. #cpp23
3 019
12
​​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 даже перегружен оператор вывода в поток, поэтому вы спокойно можете вывести сырой трейс на консоль. Вывод может быть примерно таким: 0# bar() at /app/example.cpp:5 [0x4033ee] 1# foo() at /app/example.cpp:10 [0x40340d] 2# main at /app/example.cpp:14 [0x403428] 3# <unknown> [0x74eed1a2a1c9] 4# libc_start_main [0x74eed1a2a28a] 5# start [0x403304] Вот примерчик, чтобы поиграться. Теперь легко сделать свое исключение со стектрейсом и прочими прелестями. class TracedException : public std::exception { std::string message; std::stacktrace trace; mutable std::string full_what; // cached formatted output public: explicit TracedException(std::string msg) : message_(std::move(msg)) // Capture the stack RIGHT when the exception is created // skip = 1 means skip this constructor from the trace , trace_(std::stacktrace::current(/skip=/1)) {} const std::stacktrace& trace() const noexcept { return trace_; } const char* what() const noexcept override { if (full_what_.empty()) { full_what_ = message_ + "\n\n--- Stack Trace ---\n" + std::to_string(trace_); } return full_what_.c_str(); } }; Трейс захватываем в конструкторе, собственно ровно в том месте, откуда и летит исключение. И скипаем один фрейм сверху, чтобы не учитывать конструктор, с помощью параметра метода std::stacktrace::current. В целом, использование очень похоже на std::source_location. Know your origin. Stay cool. #cpp23
2 912
13
​​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
3 013
14
​​Identity объекта #опытным На пространстве интернетов мне попался шортс, где довольно опытный в С++ чувак рассказывает про некий "трюк" в контексте такой задачи: По сути, нужно построить центральный сервис, который отправляет обновления цен на станции, но новые желаемые цены могут поступать, пока старые обновления ещё в пути. Вас волнует только то, чтобы каждая станция в конечном итоге получила самую последнюю желаемую цену, при этом подтверждения (acknowledgements) должны приходить по порядку, а лишние обновления следует избегать. Таким образом, обновления, которые вы отправляете, могут прибывать на станцию не по порядку, но подтверждения, которые вы получаете обратно, всегда должны быть правильно упорядочены в том порядке, в котором станция применила обновление цены. И, по сути, спроектировать класс так, чтобы минимизировать время и тд. трюк в том, что если вам нужен идентификатор объекта, то вы можете использовать его уникальный адрес. Приводился такой пример: struct StationClient { [[nodiscard]] uintptr_t ClientId() const { return reinterpret_cast<uintptr_t>(this); } }; Выглядит с первого взгляда круто. Действительно, у каждого объекта свой уникальный адрес. Два объекта в программе не могут занимать одну ячейку памяти. Получается, что адрес можно использовать как id и это "просто работает". Только вот сходу можно назвать кучу проблем, к которому ведет этот подход: 🔞 id объекта меняется, если он перемещается 🔞 id объекта меняется, если он копируется(должны ли такие объекты копироваться и перемещать вообще - это вопрос, но проблемы остаются) 🔞 такой айди тесно связан с временем жизни объекта и не существует о отрыве объекта в программе 🔞 id переиспользуется другими объектами и на стеке, и на куче. Как только разрушился объект, новый объект может получить тот же самый id. И как различить объекты в таком случае непонятно. 🔞 id нельзя безопасно использовать как внешний идентификатор, аля передавать в другие сервисы или сохранять в хранилища. Я понимаю, что скорее всего на интервью, где-то в отдельных модулях приложения или в каких-то игрушечных проектах такой подход может сработать. Но в каких-то больших распределенных системах вряд ли. Хотя мир большой и вы в комментариях расскажете, где такой подход применяется на практике. Если хочется что-то простое и на коленке сделанное, то можно использовать приватный статический(потенциально атомарный) счетчик внутри класса и при создании объекта инкрементировать его: struct StationClient { StationClient() { id = Id.fetch_add(1); } [[nodiscard]] std::size_t ClientId() const { return id; } private: static inline std::atomic<std::size_t> Id = 0; int id; }; При копировании, мувах Id не меняется(но это можно исправить, если надо), а чтобы новый объект заимел тот же айди нужно 100500 лет. Однако при рестарте приложения, счетчик все еще обнуляется и будут дубли. Если вам критично, чтобы не существовало двух объектов с одинаковым id, можно использовать UUIDv7. Там 2¹²² вариантов айдишников, вероятностью совпадения при использовании uuid обычно пренебрегают. Have your own identity. Stay cool. #design
3 075
15
Агенты добрались до продакшена Про 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. То есть качество можно серьезно растить не только заменой модели на новую большую и дорогую, а обвязкой вокруг нее. И эти обвязки централизовано добавляются и управляются в AGTS. Вот некоторые достижения харнесса: 🔥 +9 п.п. на BrowseComp за счет фильтрации поисковой выдачи перед подачей в модель; 🔥 +8,5 п.п. на GAIA после добавления работы с файлами, мультимодальности и OCR; 🔥 стоимость запроса снизили в 6,6 раза; 🔥 latency p90 ужали с 30 минут до 2,5 минут; 🔥 GPU на проде стало нужно в 5 раз меньше. В общем, очень ценный доклад именно с точки зрения раскрытия внутрянки такой сложной системы. Без AI мы уже никуда, но включение человека всё еще необходимо, чтобы заставить его работать с профитом и предсказуемостью на больших масштабах.
2 908
16
​​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() << ") " &lt;&lt; location.function_name() &lt;&lt; ": " << 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
2 802
17
​​Виртуальный шаблонный метод #опытным Во влажных мечтах С++ программистов всегда находится место для возможностей одновременного использования статического и динамического полиморфизмов. И тут прям напрашивается иметь виртуальный шаблонный метод. Давайте напишем: 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 невозможно. Хорошо, что в С++ есть множество других инструментов и концепций, которые помогут решить вашу техническую проблему в без виртуальных шаблонных методов. Это type erasure, crtp, visitor, std::function и std::any. Combine the incompatible. Stay cool. #cppcore #interview
3 180
18
Сегодня последний день регистрации на КосмоХакатон 18–20 сентября, Петербург, Красноярск или онлайн из любой точки страны. Че
Сегодня последний день регистрации на КосмоХакатон 18–20 сентября, Петербург, Красноярск или онлайн из любой точки страны. Четыре задачи на выбор: снабжение орбитального топливного узла и безопасность космонавта в открытом космосе в Петербурге (фонд 1,2 млн ₽), мониторинг лесных пожаров и верификация углеродных кредитов по спутниковым снимкам в Красноярске (600 тыс. ₽). Есть и для тех, кто про данные и ML, и для тех, кто про стратегию и экономику. Лучшие забирают приз на площадке и билет в московский финал 25–27 сентября, где разыграют ещё 2,4 млн рублей. Команду можно собрать на платформе. Подробности и регистрация
1 545
19
​​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; } На каждой итерации цикла идет проверка, не дошли ли мы до конца. Это нужно, чтобы мы не вышли за границы последовательности при поиске. Но что, если у нас данные в таком формате, что мы точно знаем о существовании там нужного элемента? Пусть мы парсим какие-то данные в заданном формате. Например, csv. Мы четко знаем, что между элементами всегда есть разделитель. Это либо запятая, либо символ конца строки. При чтении каждого отдельного элемента мы точно знаем, что он закончится либо запятой, либо символом конца строки. В этом случае при поиске разделителя мы точно не выйдем за границы диапазона, ведь мы четко знаем, что он там будет. В этом случае было бы довольно полезным просто выкинуть проверку на окончание диапазона. Это бы дано очень несущественный, но все же импакт в скорость программы, особенно для больших файлов. И ограничитель std::unreachable_sentinel фактически позволяет это сделать. Причина в том, что операция проверки на равенство для него всегда возвращает false. template<std::weakly_incrementable I> friend constexpr bool operator==( unreachable_sentinel_t, const I& ) noexcept { return false; } И хоть алгоритм std::ranges::find не сильно отличается по реализации от std::find (там тоже есть проверка конца последовательности), компилятор видит, что проверка конца всегда возвращает false и выкидывает ее из кода. Таким образом мы экономим одну операцию при проверке очередного элемента последовательности. Однако здесь есть очевидная проблема. Если нужный элемент по какой-то причине не будет найден в пределах заданного диапазона, то мы получаем выход за границы и выстрел UB в упор. Поэтому использовать std::unreachable_sentinel нужно очень осторожно и только в случае полной уверенности в формате данных. Be sure. Stay cool. #cpp20 #optimization
3 244
20
​​Безопасный 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
3 016