es
Feedback
Грокаем 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 367
Suscriptores
+224 horas
+27 días
+4130 días
Atraer Suscriptores
septiembre '26
septiembre '26
+76
en 0 canales
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 0 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
16 septiembre+1
15 septiembre+5
14 septiembre+6
13 septiembre+2
12 septiembre+6
11 septiembre+4
10 septiembre+8
09 septiembre+4
08 septiembre+3
07 septiembre+7
06 septiembre+7
05 septiembre+3
04 septiembre+5
03 septiembre+6
02 septiembre+7
01 septiembre+2
Publicaciones del Canal
​​Безопасный 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

2
Хакатон на космических данных. Санкт-Петербург, Красноярск или онлайн из любой точки страны. Стартуем 18 сентября. За выходны
Хакатон на космических данных. Санкт-Петербург, Красноярск или онлайн из любой точки страны. Стартуем 18 сентября. За выходные участники доводят одну из задач до рабочего решения. Санкт-Петербург. К 2035 году над Землёй должен работать топливный узел, у которого заправляются корабли, и откуда он будет получать топливо - с Земли, с Луны или из резерва - пока не решил никто. Команде предстоит собрать схему поставок, посчитать резервы и решить, во что вкладываться сразу. Призовой фонд 1,2 млн ₽. Красноярск. Спутник видит горящий лес раньше любого наземного поста, но вместе с пожаром ловит нагретые крыши, факелы на месторождениях и блики на воде. Команде предстоит научить сервис отличать настоящий очаг от шума, обвести гарь и выдать площадь в гектарах. Призовой фонд 600 тыс. ₽. Лучшие забирают приз на площадке и билет в московский финал 25–27 сентября, где разыграют ещё 2,4 млн рублей. Команда 3–5 человек, недостающих можно найти на платформе. Регистрация по ссылке космохакатон.рф
1 231
3
Базовые трейты объектов. Ч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>); static_assert(!std::is_standard_layout_v<Poly>); static_assert(!std::is_trivially_copyable_v<Poly>); static_assert(!std::is_trivially_default_constructible_v<Poly>); static_assert(!std::is_aggregate_v<Poly>); 🔀 Более менее понятно, что будет, если добавить пользовательский конструктор. А что если добавить кастомный деструктор? struct A { int a; int b; int c; ~A() {} }; static_assert(!std::is_trivially_destructible_v<A>); static_assert(!std::is_trivially_copyable_v<A>); static_assert(!std::is_trivially_copy_constructible_v<A>); static_assert(!std::is_trivially_move_constructible_v<A>); static_assert(std::is_aggregate_v<A>); static_assert(std::is_standard_layout_v<A>); Он никак не влияет на layout объекта и на способность объекта быть созданным агрегацией. Однако объект перестает быть тривиально копируемым и создаваемым копией или перемещением. С перемещением понятно - оно не генерится с кастомным деструктором. А с копированием интересно. Стандарт говорит, что при установке требований на создание объекта нужно учитывать и разрушение объекта. Поэтому объект типа А нельзя создать тривиально копией(хотя конструктор копирования продолжает генериться компилятором и быть тривиальным). Для is_trivially_copyable нужен деструктор, чтобы не было никакой дополнительной логики освобождения ресурсов при побайтовом копировании одного объекта в другой. Это все звучит, как ненужные и неважные детали. Но мы здесь грокаем С++. И просто интересно, почему, с точки зрения архитектуры языка, ломается тривиальность дефолтного деструктора при добавлении виртуальной функции. Know your traits. Stay cool. #template #cppcore
1 309
4
​​Базовые трейты объектов #новичкам Трейты(или свойства объектов) - мощнейший инструмент проверки требований для исполнения кода. Вы явно в апи через 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 если нет. Для каждого трейта определен вспомогательный шаблон переменной с суффиктом _v, который хранит непосредственно булево значение. Ну и по порядку: ✅ is_class. A действительно является полноценным классом с точки зрения С++. Структуры и классы в этом смысле не отличаются. ✅ is_aggregate. С агрегатами можно использовать инициализацию членов через список инициализации внутри {}. ✅ is_standard_layout. Очень упрощая, ваш класс не имеет виртуальных функций, все поля также являются standard_layout с одинаковым модификатором доступа и у него нет базовых классов. ✅ is_trivially_copyable. Класс имеет хотя бы один копирующий или перемещающий специальный метод и все они сгенерированы компилятором. ✅ is_trivially_destructible, is_trivially_copy_constructible,is_trivially_move_constructible, is_trivially_copy_assignable, is_trivially_move_assignable_v - соответствующий специальный метод должен быть сгенерирован компилятором. Есть также версия без trivially, которая просто проверяет наличие операции в классе. ✅ is_trivially_default_constructible - дефолтный конструктор сгенерен компилятором и внутри него не выполняется никакой код(все поля типа должны иметь тривиальный дефолтный контруктор, то есть состоять только из тривиальных типов). ✅ is_object -  Все, что не ссылка, не функция и не void, то объект. Интересно, что A - это и класс, и объект. Вот такое ООП в плюсах, которые мы заслужили. ✅ is_compound - указатели, ссылки, массивы, функции, классы и объединения, перечисления, и их cv-квалификации. Дальше пойдут трейты, которым не удовлетворяет простая структура: 🙈 is_empty - имеет хотя бы одно нестатическое поле. 🙈 is_polymorphic - есть хотя бы одна виртуальная функция 🙈 is_fundamental_v - арифметические типы, void и std::nullptr_t. 🙈 is_scalar - все, что можно представить числом. Числа, указатели, перечисления. В следующем посте можем обсудить, как ситуация изменится, если чуть подкрутить начальную структуру. Know your traits. Stay cool. #template #cppcore
2 035
5
​​2 способа остановки воркеров #опытным Более менее всех опытные плюсовики знают классический подход к graceful остановке воркера. Это атомарный флаг, цикл внутри рабочей функции с проверкой и деструктор с установкой флага. Выглядит все примерно так: class Worker { public: Worker() : t([this] { while (!stop_flag.load(std::memory_order_relaxed)) { std::cout << "Working\n"; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } std::cout << "Stopped\n"; }) {} ~Worker() { stop(); } void stop() { stop_flag.store(true, std::memory_order_relaxed); if (t.joinable()) { t.join(); } } private: std::atomic<bool> stop_flag = false; std::thread t; }; Конечно, там нужно еще куча кода, чтобы заставить воркер полезную работу делать, но сейчас это не важно. Важно, что есть тред и есть отдельная атомарная переменная, сигнализирующая об останове. Но мы(и С++) выросли и появился новый инструмент останова воркера - std::jthread + std::stop_token. Да. std::jthread - это не просто тред, который джойнится в деструкторе. Это тред с вшитой функциональностью остановки. В отличие от std::thread, jthread содержит внутреннее приватное поле типа std::stop_source, которое хранит разделяемое состояние остановки потока. Из этого stop_source можно создать токены std::stop_token, через которые можно мониторить, остановится поток или нет. Грубо говоря, std::stop_source это ручка, за которую можно дернуть и перевести все ассоциированые с ней токены в состояние "остановка запрошена". С токенами можно и отдельно работать, а можно использовать чисто внутри jthread. class Worker { public: Worker() : t([this](std::stop_token st) { while (!st.stop_requested()) { // полезная работа... std::cout << "Working\n"; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } std::cout << "Stopped cleanly\n"; }) {} // Деструктор автоматически вызовет request_stop() и join() private: std::jthread t; }; Конструктор jthread принимает функцию, первым аргументом которой является std::stop_token; этот токен будет передан объектом jthread из его внутреннего std::stop_source. Это позволяет функции проверять во время выполнения, был ли запрошен останов, и завершаться, если запрос поступил. Ручка std::stop_source дергается в момент вызова деструктора соответствующего jthread'а и сигнализирует всем токенам, что останов запрошен. То есть начиная с С++20 механизм останова полностью встроен внутрь самого объекта и никаких дополнительных сущностей вводить не нужно. Evolve. Stay cool. #cpp20 #concurrency
2 140
6
КосмоХакатон 11–13 сентября. Кейс на спутниковых данных 🚀 Два формата: очно в Благовещенске (ДФО) или онлайн из любой точки
КосмоХакатон 11–13 сентября. Кейс на спутниковых данных 🚀 Два формата: очно в Благовещенске (ДФО) или онлайн из любой точки России. Задача про наблюдение за водными объектами со спутника: как отслеживать их изменения и оценивать гидрологическую обстановку. Паводки, обмеление, разливы. Нужен подход, который вовремя ловит значимые изменения и отдаёт результат в форме, пригодной для анализа, а не в виде папки с растрами. 🏆 Призовой фонд площадки — 600 000 ₽ 🛰 Команды от 3 до 5 человек. Собери свою или присоединяйся к другим на нашей платформе. 🚀 Лучшие команды выходят в финал в Москве 25–27 сентября — ещё 2,4 млн ₽, и туда тоже можно подключиться онлайн Детали и расписание: космохакатон.рф/blagoveshchensk
1 919
7
​​Результаты ревью #опытным Спасибо всем комментаторам за актив и зоркие глаза! Да, да, ревью - это не только про знания, но еще и про умение замечать все мельчайшие недочеты. Но это лирика, погнали по проблемам. Напомню код: #include <atomic> #include <mutex> #include <queue> #include <thread> struct Worker { Worker() : t([](std::stop_source st) { while (!st.stop_requested()) { std::lock_guard<std::mutex> lock(m); q.push(1); } }) {} std::thread t; std::mutex m; std::queue<int> q; }; int main() { Worker w; } Начнем с простых и дойдем до серьезных: 🔞 Лишний хэдэр, соответственно лишнее время, которое тратится при компиляции на его анализ. 🔞 Внутри коллбэка используются поля класса, при этом захват у лямбды по умолчанию. Эта штука не заработает без захвата this. 🔞 Зачем-то в цикле идет захват лока, хотя никакой реальной конкурентной обработки очереди в данном конкретном случае нет. Его просто можно выкинуть. 🔞 Поток-то мы создали, а завершаться он как будет? std::thread всегда нужно мануально завершать. Но вообще говоря, токены останова сильно намекают на jthread'ы, поэтому можно их использовать и все заведется. 🔞 Коллбэк запуска потоков принимает std::stop_source. Для того, чтобы механизм остановки через стоп-сигналы из С++20 работал, коллбэку std::jthread нужно принимать std::stop_token. 🔞 Ну и основная "нетривиальная" проблема в этом коде. При создании потока в списке инициализации мы захватываем по сути еще неготовый объект. Мьютекс и очередь еще даже по умолчанию не инициализированы. И поток вполне может запуститься со ссылкой на эти неинициализированные данные. А это уже UB. 🔞 В ту же колоду отходит проблема при разрушении объекта. Даже, если мы сделаем jthread и он сможет хоть как-то разрушиться. Вызовы деструкторов полей класса происходят в порядке обратном объявлению. А значит очередь и мьютекс разрушаться до вызова деструктора потока. Что опять же приводит к dangling reference и ub. Пофиксить просто - объявляем поток в самом низу и дело в шляпе. Вот исправленная версия: #include <queue> #include <thread> struct Worker { Worker() : t([this](std::stop_token st) { while (!st.stop_requested()) { q.push(1); } }) {} std::queue<int> q; std::jthread t; }; int main() { Worker w; } Спасибо, @vm05s24 и @thonease за пристальное ревью) Fix your flaws. Stay cool.
2 816
8
​​Ревью #опытным Как-то мы обходили стороной некоторые обновления в стандартной concurrency библиотеке С++. Можно было бы начать прям сразу, но зайдем с небольшого интерактива в рамках рубрики #ревью. Все просто: мы даем небольшой кусочек кода, а вы накидываетесь и оставляете комменты о том, что в этом коде на ваш взгляд не так. Все как в стандартном процессе кодревью. Единственное, надо учитывать формат "небольшого кусочка кода", который тяжело сделать практически полезным. Вот и сам код: #include <atomic> #include <mutex> #include <queue> #include <thread> struct Worker { Worker() : t([](std::stop_source st) { while (!st.stop_requested()) { std::lock_guard<std::mutex> lock(m); q.push(1); } }) {} std::thread t; std::mutex m; std::queue<int> q; }; int main() { Worker w; } Комментатора с самым большим количеством корректно отмеченных проблем упомянем в завтрашнем посте с разбором. Раз, два, три, код в порядок приведи! Critique your decisions. Stay cool.
2 809
9
​​Какой метод не переопределяется? #новичкам Для начала. Что будет если не пометить переопределенный в дочернем классе метод как override? Обычно говорят, что override генерирует проверку компилятора, что данный метод действительно переопределяет метод базового класса. И соотвественно ошибку в случае, если это не так. Проверки - это дело важное и нужное, их нужно использовать. Но в том-то и дело, что override - это лишь проверка. Она никак не влияет на то, реально ли переопределяется метод или нет. Взглядните на этот код: struct Base { virtual void foo(int i) const { std::cout << "Base" << std::endl; } }; struct Derived : Base { void foo(int) const { std::cout << "Derived" << std::endl; } }; int main() { std::unique_ptr<Base> p = std::make_unique<Derived>(); p->foo(42); } Является ли метод foo в Derived переопределением метода из Base? Казалось бы, нет пометок ни virtual, ни override. Но это и не важно. Главное, чтобы сигнатура метода из дочернего класса была ровно такой же, какой была сигнатура виртуального метода в базовом классе. Здесь с этим все в порядке, поэтому метод действительно переопределяется.. То есть метод не переопределяется только в случае отличной сигнатуры. Поставьте в этом случае override и тогда будет ошибка компиляции. Поставьте в наследнике virtual и сделаете новый метод виртуальный метод в наследнике. Ничего не делаете и получите просто новый невиртуальный метод в наследнике. Что же влияет на сигнатуру? 1️⃣ Имя функции и набор параметров. Это банально и все знают. 2️⃣ CV-квалификация метода. Грубо говоря, константный метод или нет. 3️⃣ Ref-квалификация метода. Говорили о них тут. Если что-то из этого не соответствует виртуальному методу в родительском классе, то дочерний метод не будет переопределять родителя. Use checks. Stay cool. #cppcore
3 304
10
​​Человеческие сообщения об ошибках в static_assert #опытным Одной неприятной особенностью static_assert еще с С++11 были ограничения на сообщения об ошибках. По сути могли использоваться только строковые литералы. ТО есть просто фиксированные строки. Никаких динамических преобразований: template <class T> void g() { static_assert(sizeof(T) == 4, "Type T size is not 4"); } Из сообщения об ошибке компиляции: error: static assertion failed: Type T size is not 4 мы узнаем лишь то, что размер не равен 4. Хотя, вообще говоря, было бы неплохо увидеть размер переданного типа прямо в сообщении об ошибке для упрощения дебага. Но сделать мы этого не могли, даже константные выражения не могли использоваться. До С++26, когда завезли константные выражения в сообщения об ошибках. После запятой может быть объект msg, у которого должны быть 2 constexpr метода .data() для получения указателя на начало строки и .size() для получения размера. Это позволило использовать различные библиотеки форматирования внутри сообщения об ошибке. Например std::format: template <class T> void f() { static_assert( sizeof(T) == 4, std::format("Type T size should be 4, actual: {}", sizeof(T)) ); } int main() { f<std::int32_t>(); f<std::int64_t>(); } И это работает уже на gcc. Так что внутри static_assert теперь разрешены любые выкрутасы, главное чтобы они были константными выражениями. Be flexible. Stay cool. #cpp26
3 053
11
❓Кто этот призрак в вашем коде: изучаем особенности работы с легаси на C++ Легаси — вот что объединяет всех разработчиков на
❓Кто этот призрак в вашем коде: изучаем особенности работы с легаси на C++ Легаси — вот что объединяет всех разработчиков на всех языках программирования. Мы учимся уживаться с «наследием», встраиваем его в современную кодовую базу или стараемся не трогать. Пришло время разобраться с легаси в email-проекте Ghost in the code. Вы получите семь писем от инженеров, в числе которых представитель России в Международной рабочей группе по стандартизации C++ Антон Полухин и эксперт по архитектуре Константин Владимиров. Вместе с ними и другими опытным разработчиками пройдете путь от навигации по «зрелому» коду с помощью AI до выстраивания процессов с учетом легаси. ➡️Подписывайтесь на серию писем, это бесплатно. Оставляйте email-адрес на сайте проекта — первое письмо придет 15 сентября.
2 652
12
​​Какой тип nullptr? #опытным Для многих nullptr - это магическая хреновина, которая магически и правильно "зануляет"(что бы это не значило) указатель. А как это работает - уже не важно. И даже на этом уровне понимания можно прекрасно писать хороший код и не знать забот. Но мы здесь собрались для грокания С++, поэтому будем разбираться. Для начала, nullptr - это литерал указателя. Есть литералы целочисленые(42, -100), есть литералы строковые("123", "qwerty"), а есть литерал указателя и nullptr - его единственный представитель. Литерал - это prvalue. Этот литерал обозначает константу нулевого указателя. То есть при присваивании указателю nullptr'а, он зануляется. Происходит это потому что существует неявное преобразование nullptr к значению нулевого указателя любого указательного типа. Но это не единственная константа нулевого указателя. Есть также литералы 0 и NULL. У каждого литерала есть свой тип. У nullptr это std::nullptr_t. А у 0 и NULL - int. И этом кроется вся суть: void g(int*) { std::cout << "Function g called\n"; } int main() { g(nullptr); // Fine g(NULL); // Fine g(0); // Fine // But auto null1 = nullptr; auto null2 = NULL; auto null3 = 0; g(null1); // Fine g(null2); // ERROR: non-literal zero cannot be a null pointer constant g(null3); // ERROR: non-literal zero cannot be a null pointer constant } Как только мы присвоили NULL или 0 какой-то переменной, они потеряли способность инициализировать указатели. Потому что они просто стали какой-то переменной типа int, а какими-то рандомными числами очень странно инициализировать указатели. Но с nullptr другая штука. null1 - это переменная типа std::nullptr_t. И здесь нет никакой неоднозначности. Этот тип придуман для того, чтобы представлять нулевой указатель. Он больше ни для чего не нужен. Поэтому также есть неявные преобразование любых значений типа std::nullptr_t к значению нулевого указателя. Это значит, сколько бы раз не был прокинут nullptr в разные переменные, он всегда будет означать именно нулевой указатель. Don't be ambiguous. Stay cool. #cpp11
3 152
13
​​Критическая секция. Примитив синхронизации #опытным Стандартный ответ на обычный вопрос "а какие примитивы синхронизации вы знаете?" - мьютексы, атомарные переменные, условные переменные. Для них есть привычные инструменты в языке - std::mutex, std::atomic и std::condition_variable. Иногда проскальзывают в ответах более опытных людей людей семафоры и барьеры - std::counting_semophore и std::barrier. Но очень очень редко кто-то да упомянет критическую секцию. Если вы пишите только на плюсах и кроссплатформенный код, то вряд ли вообще слышали про этот примитив. В стандартной библиотеке нет класса std::critical_section. Тем не менее такой примитив действительно есть, просто в чистом Windows API. По названию понятно, что примитив CRITICAL_SECTION защищает блок кода (критическую секцию в первом значении) от одновременного исполнения более чем одним потоком. Но общепринятый примив для такой ситуации - мьютекс. В чем тогда разница CRITICAL_SECTION и мьютекса? Сейчас мы отходим от языка и погружаемся в системное апи. Оба эти примитива выполняют одинаковую функцию - обеспечивают взаимное исключение потоков. Разница в нюансах Мьютекс может быть использован для синхронизации между несколькими процессами. CRITICAL_SECTION помогает синхронизировать потоки только внутри одного процесса. CRITICAL_SECTION также чуть лучше перформит за счет того, что перед блокировкой потока там стоит спинлок, который позволяет избежать дорогого обращение к ядру для помещения потока в очередь ожидания. Вот и вся разница. Наиболее близким аналогом CRITICAL_SECTION в линуксе можно считать futex, в котором дорогостоящий системный вызов происходит только, когда несколько потоков прям вот сейчас пытаются войти в защищаемый блок кода. На уровне С++ мы имеем все тот же std::mutex, который скорее всего также использует эту оптимизацию, поэтому в языке про CRITICAL_SECTION ни сном, ни духом. Think critical. Stay cool. #concurrency
2 791
14
Приглашаем на бесплатный открытый вебинар курса «Программист С»: «С без компромиссов: обработка ошибок и полиморфизм» Когда:
Приглашаем на бесплатный открытый вебинар курса «Программист С»: «С без компромиссов: обработка ошибок и полиморфизм»   Когда: 25 августа, 20:00 (мск) Язык С даёт полный контроль над памятью и железом, но за эту мощь приходится платить — в нём нет встроенных исключений и классов. Однако, это не значит, что отказоустойчивость и гибкость недоступны. На вебинаре разберём техники, которые обычно остаются за кадром, и покажем, как писать на С надёжно, масштабируемо и без ущерба для производительности. Что будет на вебинаре: Рассмотрим setjmp/longjmp — аналог исключений, который позволяет «телепортироваться» сквозь стек вызовов; Создадим виртуальные таблицы (vtable) — руками реализуем полиморфизм, как в C++; Соединим обе техники в одном проекте – создадим систему логирования с централизованной обработкой ошибок. Зарегистрироваться https://otus.pw/4vWb/ Бесплатное занятие приурочено к старту курса «Программист С», на котором вы освоите продвинутые приёмы работы с памятью, многопоточностью, сетевым взаимодействием и созданием высоконагруженных систем на чистом С. Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
1 871
15
​​Критическая секция. Блок кода #новичкам Термин "критическая секция" имеет 2 значения и это несколько конфузит как новичков, так и опытных разрабов, кто просто не встречался с двумя личинами термина. Сейчас и в следующем посте разберем оба значения. Начнем с простого. Одна из самых больших опасностей многопоточного кода - это гонка данных. Когда 2 потока пытаются читать и записывать в одну ячейку памяти без использования синхронизации. Проблема гонки на самом деле в том, что потоки могут увидеть промежуточное состояние комплексной операции и вклиниться посередине. Даже банальный инкремент инта это 3 операции: чтение, модификация и запись. int counter = 0; // unprotected variable void increment() { for (int i = 0; i < 10000; ++i) { ++counter; // data race } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); // expected 20000, but there is data race so counter will never be eq to the number std::cout << "Final counter value: " << counter << std::endl; return 0; } Но если мы уже будем говорить про такие комплексные операции, как вставка элемента в контейнер, то проблема становится очень явной. Невозможно потокобезопасно, например, проверить превышение size на capacity в векторе, выделить новую память, перенести туда все элементы вектора и создать новый объект. И чтобы между этих операций в контейнер не вмешался другой поток. Чуть более понятный пример: struct Balance { void withdraw_balance(int amount) { if (balance_ >= amount { balance_ -= amount; } } private: int balance_; }; Если 2 потока одновременно зайдут в эту функцию, то оба уменьшат баланс. Мало того, что с клиента снимут больше денег, так он еще и должным может остаться(баланс уйдет в минуса). То есть существуют участки кода, внутри которых происходит доступ к разделяемому ресурсу и там каждый момент времени может исполняться максимум один поток. Такие участки кода называют критическими секциями. Критические секции должны быть защищены примитивами синхронизации, обеспечивающими взаимное исключение потоков. Например, мьютекс: struct Balance { vvoid withdraw_balance(int amount) { std::lock_guard lg{mtx_}; if (balance_ >= amount { balance_ -= amount; } } private: int balance_; std::mutex mtx_; }; Критической секцией также может быть только часть функции: template<typename T> class ThreadSafeQueue { std::queue<T> queue_; std::mutex mtx_; public: std::queue<T> pop_all() { std::queue<T> result; { // critical section begin std::lock_guard<std::mutex> lock(mtx_); std::swap(result, queue_); } // critical section end return result; } }; В этом случае очень удобно обозначать ее вложеным скоупом (фигурными скобками) для лучшего выделения на фоне остального кода. Также выход из блока кода триггерит разрушение его локальных объектов, поэтому вам в большинстве случаев не нужно явно вызывать mtx.unlock(). Критические секции стоит делать как можно меньшего размера, чтобы время блокировки потоков было минимально. Спасибо @antom1n за идею для поста Think critical. Stay cool. #concurrency
3 125
16
​​Гибридные вектора #опытным std::inplace_vector воплощает в себя преимущества динамического расширения размеров и отсутствия аллокаций. Полезная штука, но и она ограничена. Literally: вы не можете добавить в массив элементов больше, чем capacity. И это нормальные ограничения. Нельзя иметь неограничено расширяемый массив на стеке. Но что если я знаю, что в 99% случаев мой массив будет содержать не больше N элементов. Но все же иногда нужно будет хранить потенциально намного больше элементов. Что делать? Ну для начала есть small object optimization для стандартного std::vector. Вместо того, чтобы хранить элементы в куче, небольшое число маленьких по размеру элементов можно хранить на стеке. И при добавлении элементов больше порога уже использовать динамические аллокации. Как SSO в std::string. Но это не гарантированная оптимизации и зависит от реализации стандартной библиотеки. Плюс невозможно управлять размером этого маленького буфера. Но можно сделать и более управляемый вариант. Пусть будет динамический контейнер, в котором мы гарантировано сможем расположить на стеке N элементов, а при превышении этого порога будет триггериться динамическое выделение памяти под буфер большего размера. Такой комбинированный подход позволяет и рыбку съесть, и на правильный стул сесть. Аллокаций на минимуме, перф суперблейзинговый и привычный интерфейс. Вот несколько представителей этого подхода: - absl::InlinedVector. - boost::container::small_vector. - llvm::SmallVector. - folly::small_vector. Combine advantages. Stay cool. #performance #memory #optimization
3 319
17
💻 АЦП в ESP32. Оцифровать сигнал, а не "погоду на Марсе". Нюансы, особенности, повышение точности Приглашаем на открытый уро
💻 АЦП в ESP32. Оцифровать сигнал, а не "погоду на Марсе". Нюансы, особенности, повышение точности Приглашаем на открытый урок. 🗓 24 августа в 20:00 МСК 🆓 Бесплатно. Урок в рамках старта курса «Embedded-разработчик». Мы рассмотрим весь пусть сигнала - от датчика до его цифровой формы в микроконтроллере ESP32. Вспомним основные параметры, погрешности АЦП, увидим их влияние "вживую", узнаем, почему иногда теоремы Котельникова недостаточно и что с этим делать? Поговорим о важности источника опорного напряжения, произведем расчет бюджета погрешностей тракта АЦП. Применим калибровку и сравним результаты до\после на АЦП, после чего рассмотрим простые методы фильтрации исходно зашумленных сигналов. Вебинар будет полезен: радиолюбителям, начинающим разработчикам электроники, инженерам-схемотехникам, разработчикам встраиваемого программного обеспечения В результате вебинара слушатели узнают: ✔️ все основные параметры АЦП и смогут увидеть их влияние вживую на примере ESP32 ✔️ особенности и нюансы схемотехники при проектировании тракта АЦП ✔️ что такое "алиасинг" и как его избежать ✔️ как провести обобщенный расчет погрешностей тракта АЦП ✔️ как откалибровать АЦП в ESP32 ✔️ простые методы фильтрации зашумленных сигналов 🔗 Ссылка на регистрацию: https://otus.pw/X5Wt/ Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
1 714
18
​​std::inplace_vector #опытным Стандартная библиотека традиционно запаздывает с внедрением полезного функционала. Вот у нас есть std::vector. Прекрасный контейнер, расширяемый. более менее все им пользуются. Однако у него есть проблема - динамические аллокации. От них не уйти. А если вам они не нужны, то вы вынуждены использовать другие инструменты. Да и еще и ограниченное использование вектора в constexpr контексте. Ну ладно. Есть std::array. Нет скрытых аллокаций, давно можно использовать в constexpr. Красота. Но как бы не так: не расширяемый он. Еще и элементы должны быть созданы сразу все и должны соответствовать требованию DefaultConstructable. Короче опять недостатки. Но в С++26 появился контейнер, который объединяет преимущества std::vector и std::array. Называется он std::inplace_vector. По сути это динамически расширяемый массив с фиксированной в compile-time емкостью: 1️⃣ Элементы массива хранятся прям внутри объекта, поэтому нет никаких дополнительных аллокаций. 2️⃣ Объект inplace_vector сразу при создании содержит буфер размера capacity. 3️⃣ Можно создать объект без элементов вообще и изменять их набор как угодно во время выполнения программы. Главное не превышать capacity. 4️⃣ Так как этот массив не предполагает дополнительных динамических аллокаций, то контейнер можно полноценно использовать в constexpr контексте. Рассмотрим небольшой примерчик, где нам нужно отфильтровать из std::array пложительные числа и возвести их в квадрат: template<size_t N> constexpr std::inplace_vector<int, N> square_positive(const std::array<int, N>& arr) { std::inplace_vector<int, N> result; for (int x : arr) { if (x > 0 && result.size() < result.capacity()) { result.push_back(x * x); } } return result; } int main() { constexpr std::array<int, 6> data = {-3, 5, -1, 7, 0, 4}; constexpr auto squares = square_positive<6, 6>(data); static_assert(squares.size() == 3); static_assert(squares.capacity() == 6); static_assert(squares[0] == 25); static_assert(squares[1] == 49); static_assert(squares[2] == 16); return 0; } Мы не можем заранее сказать, сколько элементов вернет функция square_positive. Но мы можем гарантировать, что их количество <= размеру входного массива. Создается inplace_vector пустым и элементы накидываются в него по очереди. Проверки static_assert гарантируют, что все вычисления происходят в compile-time. Еще одна особенность - у inplace_vector меньше требований к типу своих элементов: struct S { S() = delete; S(int) { } }; std::array<S, 5> arr; // compile error: S does not have a default constructor std::inplace_vector<S, 5> inplace_vec; // ok Так как std::array создает сразу все свои элементы, конструирование arr завершится ошибкой, потому что S не имеет конструктора по умолчанию. Но inplace_vec успешно создается, потому что не имеет этого требования. Также std::inplace_vector - хороший пример того, что стандартная библиотека в новых стандартах старается поддерживать апи с использованием исключений и без них. Есть принципиально 2 разных подхода к добавлению элементов в этот массив: 👉🏿 constexpr reference push_back( const T& value ) — классический вариант, который выбрасывает std::bad_alloc при попытке превысить capacity. 👉🏿 constexpr std::optional<reference> try_push_back( const T& value ) - добавляет элемент и возвращает на него ссылку или не добавляет элемент и возвращает std::nullopt. То есть ошибка обрабатывается с помощью возвращаемого значения Есть кстати еще метод constexpr reference unchecked_push_back( const T& value );, который забивает на все проверки и перекладывает эту ответственность на пользователя. В случае превышения емкости получаем UB. В общем, крутой новый контейнер, который явно найдет себе место в системах с ограничениями кучи или суперпроизводительном софте. Combine advantages. Stay cool. #cpp26 #STL
3 284
19
​​Мапим значения на типы. Ч2 #опытным Напомню проблему: enum class DataType: uint8_t { kInt32, kDouble, kUint64 }; template<typename T> T create_from_buffer(const void* buffer) { static_assert(std::is_trivially_copyable_v<T>, "Type T must be trivially copyable"); T obj; std::memcpy(&obj, buffer, sizeof(T)); return obj; } Как на основе DataType создать объект соответствующего типа? Compile-time мапа из массива, где по индексу подкапотного числа, стоящего за каждым перечислителем, позволяет получить соответствующий нужный тип и выбрать правильный обработчик. И все это за О(1) с тратами на сдвиг элемента в массиве Если же перечислителям явно присвоены числа, то этот номер не прокатит и надо думать дальше. Без шаблонной магии нам не обойтись, ведь мы хотим только добавлять маппинги, но не менять самого кода обработчиков и логику их выбора. И давайте заодно попробуем уйти от полной специализации шаблонов, как было в предыдущей части. Сделаем структуру аля ноду маппинга: template <DataType D, typename T> struct Node { static constexpr DataType key = D; using type = T; }; В структуре хранится конкретный перечислитель, получаемый из шаблонного параметра, и соответствующий ему тип. И в качестве пака шаблонных параметров нашей шаблонной функции мы будем передавать список нод Node<DataType::kInt32, int32_t>, Node<DataType::kDouble, double>, Node<DataType::kUint64, uint64_t>. В функции мы должны сделать примерно следующее. Перебираем рантайм значение enum'а на соответствие значению Key из нод пака параметров. Перебираем, пока не найдем нужный и после используем правильную ноду для получения замаппленого типа. И вот этот перебор можно делать через fold expression и короткозамкнутый оператор ||. Как только условие истинно, вычисления прекращаются. template <typename... Es> DataVariant ConvertImpl(const void* buffer, DataType type) { DataVariant result; bool found = ((Es::key == type ? (result = create_from_buffer<typename Es::type>(buffer), true) : false) || ...); if (!found) throw std::runtime_error("Unknown type"); return result; } DataVariant Convert(const void* buffer, DataType type) { return ConvertImpl< Entry<DataType::kInt32, int32_t>, Entry<DataType::kDouble, double>, Entry<DataType::kUint64, uint64_t> >(buffer, type); } Также тут используется фишка оператора запятой, что результатом выражения является только самый крайний справа операнд. Теперь вместо поиска элемента в массиве по индексу мы занимаемся линейным проходом выполнения логического ИЛИ для типов. На первый взгляд это может быть сильно дольше, но компиляторы хорошо умеют оптимизировать fold expression, даже в худшем случае большой разницы не будет. Код кстати по размеру уже не сильно больше изначального свитча. Вот примерчик с рабочим кодом, можете поиграться. Use tricks. Stay cool. #template #cpp17
3 208
20
🏎 Реши реальную задачу из закрытого мира HFT – соревнование с призовым фондом 12 000 USD и коротким треком найма Spectral::T
🏎 Реши реальную задачу из закрытого мира HFT – соревнование с призовым фондом 12 000 USD и коротким треком найма Spectral::Technologies занимается высокочастотным трейдингом (HFT) и уже более семи лет торгует на рынках по всему миру с помощью собственных стратегий и low-latency инфраструктуры. Ребята делают это крайне успешно: инженеры у них получают сотни тысяч долларов фиксом на руки, команда постоянно растет. Сейчас Spectral нанимает на вакансии C++ SWE и AI SWE (C++, Python). Чтобы вы могли лучше понять, чем занимаются их инженеры – они открыли одну из своих реальных задач в формате соревнования. Цель соревнования – разработать систему, которая передает поток данных от одного источника нескольким получателям с минимальной задержкой. Добиться низкой задержки в среднем или на медиане можно достаточно стандартными приемами. Настоящий челлендж – сделать так, чтобы на высоких квантилях и под нагрузкой задержка ухудшалась минимально, даже когда растут объем передаваемых данных и количество получателей. Вместе с участниками эту же задачу будет решать Claude-агент. Его решение будет находиться в публичном репозитории, и вы сможете использовать его как baseline. Оценивать будут распределение задержки, масштабируемость, корректность и воспроизводимость решения. Использовать AI-инструменты при разработке можно. Участие индивидуальное. Опыт в HFT необязателен, но пригодятся уверенные знания C++, системного программирования, сетей и оптимизации производительности. 🏆 Призы: 1 место – 6 000 USD gross 2 место – 3 600 USD gross 3 место – 2 400 USD gross Авторам лучших работ – и не только участникам из топ-3 – предложат сокращенный трек найма на позиции C++ Software Engineer или AI Software Engineer. Лучших участников также ждет мерч. Соревнование уже идет. Присоединиться можно в любой момент до дедлайна. ⏰ Дедлайн сдачи решений – 30 августа, 23:59 GMT+3 Регистрация и доступ к GitLab с задачей – @spectral_challenge_bot
1 113