Г
Грокаем C++
17.07.2026 17:00 · 👁 855
Сколько потоков можно запустить на одной машине?
#опытным
Из всех утюгов говорят про асинхронность вычислений и экономию ресурсов ОС. Зачем это нужно? И почему я не могу на каждую задачу запускать отдельный поток? Есть ли вообще какой-нибудь предел по количеству потоков?
Давайте раззззбираться.
Потоки - вещь хорошая и полезная. Не зря же недавно в питон таки завезли возможность их прямого использования. Но с потоками нужно быть аккуратным. Это ресурс. А ресурсы имеют свойство заканчиваться, когда их нерационально используют.
Классический пример: к вам в приложение приходит какой-то запрос на обработку. Первая мысль - а давайте под него выделим свой поток! Они же независимо и параллельно исполняются!
Ну и уже здесь мы поймали первую ошибку. Да, потоки могут параллельно исполняться. Но количество реально параллельно исполняемых потоков в системе ограничено.
И ограничено оно количеством вычислительных юнитов процессора. Максимальное число реально параллельных потоков можно узнать с помощью std::thread::hardware_concurrency().
Есть кстати технология HyperThreading от Intel, которая позволяет на одном физическом ядре получить 2 логических, увеличивая таким образом число потенциально параллельных потоков в 2 раза. Ускорения в 2 раза конечно не будет, но тем не менее.
"Ну хорошо. Не все 100500 потоков, которые я создал, исполняются параллельно. Не всегда нужно параллельное одновременное исполнение всех тредов. Почему всем не нравится много потоков?
Поток - это ресурс, который занимает место и им нужно управлять. Как минимум под каждый поток выделяется свой стек. На десктопах это 1-8 МБ. Поделите количество оперативной памяти на размер стека и получите максимально возможное количество потоков, которое в принципе может поместиться в память. В реальности количество будет еще меньше.
Есть же еще затраты на управление потоками. Их же нужно переключать и выдавать каждому подходящий квант времени. Переключает потоки шедулер ядра и на это тоже тратится процессорное время. С ростом количества потоков можно прийти к ситуации, когда затраты на управление потоками будут превышать количество полезной работы, совершаемой потоками.
Ну и в некоторых системах явно прописано ограничение на количество потоков. В линуксе, например, можно увидеть заветную числеку с помощью команды cat /proc/sys/kernel/threads-max.
Асинхронность - это в первую очередь про асинхронный ввод-вывод. Тут все завязано на событийно ориентированное апи системы и файловые дискрипторы, которых фактически может быть гораздо больше, чем количество потоков.
Если у вас есть сервис, который активно работает с сетью - без асинхронности не обойтись. Подход с созданием отдельного потока на каждый запрос перестанет адекватно масштабироваться уже после сотни другой одновременных подключений.
Scale up. Stay cool.
#concurrency #OS
Г
Грокаем C++
17.07.2026 14:00 · 👁 1.3K
1 августа Яндекс приглашает разработчиков на бэкенд-конференцию Back to Back
Мероприятие пройдёт офлайн и онлайн в Москве, Белграде и Ереване. Обсудим производительность, системную разработку и инженерные задачи на C++, а еще архитектуру и реальные продакшен-задачи.
Часть программы в Москве:
— Как устроена архитектура аутентификатора в гетерогенной системе с экспертом по разработке ПО YADRO Даниилом Подольским.
— Проектирование безопасного механизма отмены для C++20-корутин вместе со старшим разработчиком Яндекса Раедом Романовым.
—Доклад о том, что должен знать С++ программист про ABI с Константином Владимировым и Елизаветой Носковой из Syntacore.
У каждого города — своя программа. Если собираетесь посетить ивент в Москве, то сможете посетить экспертные сессии 1:1 c разбором личных и карьерных запросов и выступление питерской группы «Научно-технический рэп».
Регистрируемся тут.
Г
Грокаем C++
14.07.2026 09:00 · 👁 1.9K
Зачем нужен деструктор?
#новичкам
На самом деле даже разработчики с опытом не всегда правильно отвечают на вопрос. Потому что все эти конструкторы и деструкторы крутятся вокруг ресурсов. Но вот каких именно? Давайте разбираться.
Несколько раз слышал ответ: "освобождает память, занятую объектом".
Следом идет вопрос: - "То есть деструктор деаллоцирует память, на месте которой находится объект?". После положительного ответа я привожу пару примеров:
void foo() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << vec.empty();
}
Когда в этом случае вызывается деструктор и когда и как происходит деаллокация памяти, занимаемой vec?
И еще:
void foo() {
using ArrType = std::array<int, 5>;
auto * ptr = new ArrType{0, 1, 2, 3, 4};
ptr->~ArrType();
}
Происходит ли деаллокация динамической памяти в этом случае? Что здесь делает деструктор?
На самом деле деструктор просто семантически разрушает объект. Собственно это и есть значение слова "destructor" - разрушитель.
На такие темы надо рассуждать с точки зрения лайфтайма объекта. Конструктор создает объект и начинает его время жизни(даже если он ничего не делает). Деструктор же разрушает объект и заканчивает его время жизни.
Заметьте, пока про ресурсы ни слова.
Будут ли хоть какие-то ресурсы у объекта такого класса?
struct A {
int i;
char c;
};
Нет. Хотя у него есть конструктор и деструктор. Да, тривиальные, но есть же.
Когда я делаю:
void foo() {
A obj;
}
foo();
Вызывается и конструктор, и деструктор(при выходе из скоупа функции). Аллокацию и деаллокацию памяти под obj выполняет сам код, обеспечивающий запуск функции.
Разговор о ресурсах начинается тогда, когда в логике объекта заложено обладание ими. std::vector владеет указателем на динамическую память и за счет этого может масштабировать количество элементов в рантайме.
Какой-нибудь коннекшен к базе владеет открытым сокетом, через который он может общаться с ней.
Только тогда, когда объект действительно единолично владеет каким-то ресурсом, при разрушении объекта нужно этот ресурс освободить. И это не всегда память! Файловые дескрипторы и потоки - это тоже ресурсы и будет плохо, если их не освобождать.
Вернемся к примерам из начала поста.
void foo() {
using ArrType = std::array<int, 5>; // just for explicit destructor call
auto * ptr = new ArrType{0, 1, 2, 3, 4};
ptr->~ArrType();
}
std::array - это тонкая обертка над сишным массивом. Для его работы не нужны дополнительные ресурсы: его элементы располагаются ровно там, где аллоцировали память под сам объект. Поэтому и деструктор у него ничего не делает.
В примере вообще нет деаллокаций динамической памяти, так как в нем отсутствует delete, который и занимается деаллокациями.
Теперь другой:
void foo() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << vec.empty();
}
vec располагается на стеке, поэтому для этого объекта автоматически выделяется и освобождается память, просто за счет увеличения и уменьшения указателя на стек.
Так как вектор владеет ресурсом и следует идиоме RAII, динамическая аллокация происходит внутри конструктора, а деаллокация - внутри деструктора.
Но эти аллокации не затрагивают память самого объекта vec. Память под объект - это стековая память и ей управляет рантайм.
Итого: деструктор нужен для семантического разрушения объекта и заканчивает его время жизни. Деструктор, как приличный джентельмен. должен освободить все ресурсы, которыми объект овладел за время своей жизни. Но он никак не ответственен за деаллокацию памяти под сам объект. Этим занимается либо рантайм, либо программист(явно вызывая delete).
Understand the essence. Stay cool.
#cppcore
Г
Грокаем C++
13.07.2026 10:00 · 👁 2K
Tricky move
#новичкам
В прошлом посте специально была допущена определенная неточность. Скомпилировав этот код:
struct Example {
~Example() {}
};
Example a;
Example b;
a = std::move(b);
Вы не получите ошибку компиляции и все запустится без проблем.
Просто код будет работать не совсем так, как вы ожидаете.
Вы резонным образом ожидаете, что в последней строчке вызовется присваивание перемещением.
Однако вызовется копирующее присваивание.
#ЧЗХ?
В том же посте мы выяснили, что определение деструктора запрещает генерироваться перемещающим операциям.
Но не мешает копирующим!
Ну и дальше классика. В С++ если вы ожидаете перемещения чего-либо при вызове std::move - вините свои ожидания, когда все идет по звезде.
Результат вызова std::move - rvalue reference aka &&. А правые ссылки спокойно биндятся к const &. И именно такой тип параметра принимает копирующее присваивание.
Есть подходящий кандидат для перегрузки, он и вызывается.
Еще один прикол на стыке легаси С++ и его модерновой версией.
See through tricks. Stay cool.
#cpp11
Г
Грокаем C++
13.07.2026 07:00 · 👁 1.8K
👨💻На работе код читают чаще, чем пишут. Когда смысл приходится восстанавливать по комментариям, неочевидным параметрам и скрытым договорённостям, команда теряет время, а риск ошибок растёт.
📆16 июля в 20:00 МСК на открытом уроке разберем, как переносить смысл программы в типы, сигнатуры и структуру кода. На примерах из реальных проектов участники сделают сигнатуры понятнее с помощью std::optional и enum class, упростят управление ресурсами через RAII и заменят сложные циклы готовыми алгоритмами, ranges и views.
В результате у вас появится набор приёмов для более ясного и самодокументируемого кода.
🏁Урок проходит в преддверии старта курса «C++-разработчик». Зарегистрируйтесь, чтобы познакомиться с форматом обучения, задать вопросы эксперту и потренироваться выражать намерения средствами языка, а не комментариями: https://otus.pw/y5Qj/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
Г
Грокаем C++
09.07.2026 09:00 · 👁 2.4K
Когда компилятор не сгенерирует 5 специальных методов?
#опытным
После того, как в С++11 появилась семантика перемещения владения ресурсами, появились также объекты, которые единолично владеют определенным ресурсом. Они настолько не хотят им делиться с другими, что семантика копирования для них стала неприемлемой. Поэтому между новыми(перемещающими) и старыми(копирующими) специальными методами классов появилось некое противоборство - отказ компилятора автоматически генерировать определенные методы при определенных условиях.
Вам в любом случае стоит придерживаться правил 0 и 5, когда проектируете интерфейс создания и уничтожения объектов. Но все равно полезно знать, что будет если правила не выполнять.
Так когда же компилятор не будет генерировать каждый из специальных методов? Пост в первую очередь про взаимосвязь специальных методов, другие причины упоминаться не будут.
1️⃣ Деструктор
Деструктор компилятор может всегда сгенерировать. Оно в целом понятно: объект может как-то создаваться, поэтому и должен уметь как-то уничтожаться. Если вы не делаете ничего экзотического, компилятор предоставит вам деструктор. Другой вопрос, правильно ли он будет работать.
Банальный пример:
struct Bad {
Bad() : p{new int{5}} {}
Bad(Bad&& other) {
p = other.p;
other.p = nullptr;
}
int * p;
};
{
Bad b;
} // memory leak
Очевидно, что мы по коду по-особенному управляем указателем. Но компилятор все равно сам сгенерирует деструктор, который ничего не освободит и мы получим утечку памяти.
2️⃣ Конструктор копирования и копирующий оператор присваивания
Если в классе определен хотя бы один перемещающий специальный метод, то ни один копирующий метод не генерируется.
struct Example {
Example() = default;
Example(Example&&) {}
};
Example a;
Example b = a; // Error: copy ctor is implicitly declared as deleted
Example c;
c = a; // Error: copy assign operator is implicitly declared as deleted
Но это не потому что компилятор такой вредный. Если вы своими ручками определили перемещающие операции, но не определили копирующие, то вы скорее всего и не хотите, чтобы объекты можно было копировать. Но даже если и хотите, то компилятор уже понимает, что поверхностное копирование полей вам не подойдет и просто предостерегает вас от проблем.
При этом если вы определили деструктор, то копирующий операции все равно сгенерируются.
struct Bad {
Bad() : p{new int{5}} {}
~Bad() {delete p;}
int * p;
};
{
Bad b;
Bad b1 = b;
} // double free
Простейший пример и сразу же ловим двойное освобождение.
Такое поведение - наследие от более ранних стандартов, когда было правило 3-х, но оно было только на словах. Даже если деструктор делает нетривиальные вещи, то копирующие операции генерировались.
Конечно, это unsafe. Но обратная совместимость заставляет С++ нести эти особенности в новые стандарты.
3️⃣ Конструктор перемещения и перемещающее присваивание
Это те 2 новых специальных метода, которые добавили в С++11. Для новых вещей стандарт может устанавливать новые условия, которые не несут груз ответственности за обратную совместимость.
Поэтому для перемещающих операций все просто: если любой из 4-х оставшихся специальных метода определен пользователем, то компилятор не генерирует данную операцию.
struct Example {
~Example() {}
// or Example(Example&& other) {}
// or Example& operator=(const Example&) {}
// or Example& operator=(Example&&) {}
};
Example a;
Example b;
a = std::move(b); // Error: no move assign
Оно и понятно: если вы что-то сами определяете, значит хотите чего-то особенного. В этом плане компилятор усиливает правило 5: теперь вы обязаны сами определить перемещающие операций, если определяете другие специальные методы.
Be special. Stay cool.
#cpp11
Г
Грокаем C++
08.07.2026 10:00 · 👁 2.2K
Hardening
#опытным
Раз уж в прошлом посте заикнулись про hardening, давайте разберем его чуть подробнее.
Неопределенное поведение (UB) в C++ - самая ужасная категория ошибок. UB может бесшумно повреждать память, вызывать сбои в местах очень отдаленных от фактической ошибки, или, что хуже всего, просто работать на вашей машине долгое время без спецэффектов. Значительная доля UB в реальных кодовых базах происходит не от экзотических языковых функций, а от базового неправильного использования стандартной библиотеки: доступа к вектору за пределами границ, вызова front() на пустом контейнере или вызова метода на пустом std::optional.
C++26 частично решает эту проблему напрямую с помощью харденинга стандартной библиотеки
Что это за зверь?
Харденинг библиотеки преобразует определённое неопределённое поведение в стандартной библиотеке в обнаруживаемые нарушения контрактов во время выполнения. Когда нарушается харденизированное предусловие, среда выполнения реагирует до того, как произойдут какие-либо другие наблюдаемые побочные эффекты.
То есть, раньше вся ответственность за корректное использование методов ложилось на плечи программистов. Допускаешь доступ за границы массива - жди беды, тебе о ней компилятор и рантайм не сообщат.
Теперь же реализации стандартной библиотеки вставлять специальные проверки, неудовлетворение которой ведет к предсказуемому завершению программы. И вроде как даже предоставляются инструменты для понимания, где произошла "паника".
Это не новая идея. Все три основные реализации стандартной библиотеки уже поставляют свои собственные режимы харденинга, зависящие от конкретного вендора. Проблема в том, что эти механизмы различны, непереносимы и не имеют единой спецификации. Теперь это дело стандартизировано.
Примеры:
std::vector<int> v = {1, 2, 3};
// нарушение контракта: 5 >= 3
int x = v[5];
v.pop_back();
v.pop_back();
v.pop_back();
// нарушение контракта: нельзя убрать элемент из пустого вектора
v.pop_back();
std::string_view sv("hello");
// нарушение контракта: 10 >= 5
char c = sv[10];
// нарушение контракта: 10 > 5
sv.remove_prefix(10);
std::optional<int> opt;
// нарушение контракта: нет реального объекта
int x = *opt;
int data[10];
// нарушение контракта: разное число элементов
// в шаблонном параметре и в аргументе конструктора
std::span<int, 5> sp(data, 3);
// нарушение контракта: 10 > size()
sp.first<10>();
Сильнейший аргумент в пользу этой включения харденинга — производственный опыт Google, на который ссылается пропоузал: применение харденизированного libc++ в «сотнях миллионов строк C++» выявило более 1000 ошибок, включая критически важные для безопасности. Средние накладные расходы на производительность оказались удивительно низкими — 0,30%(одна треть процента). Эти накладные расходы остались такими низкими благодаря способности компилятора устранять избыточные проверки во время оптимизации.
Влияние вышло за рамки безопасности: команды наблюдали 30%‑е снижение базового уровня сегментационных ошибок (segfault) в продуктивной среде, что указывает на повышение корректности кода в целом.
У gcc и msvc сейчас только частично поддержан hardening, но относительно скоро все мы сможем потратить год на исправление всех найденных уязвимостей насладиться более безопасным кодом.
Be safe. Stay cool.
#cpp26 #compiler
Г
Грокаем C++
07.07.2026 10:05 · 👁 2.1K
Такие разные векторы
#опытным
Про std::vector сказано многое, но он не перестает удивлять.
Вектор владеет регионом динамической памяти и ему надо трекать, где заканчиваются текущие элементы и где в принципе заканчивается регион.
Наивное представление, которое напишет примерно любой джун:
template<typename T>
class Vector {
...
T * begin;
size_t size;
size_t capacity;
};
Понятно, что в реальности был бы еще аллокатор, тип указателя был бы зависимым типом аллокатора и скорее всего был бы char *, но смысл один: есть указатель на начало и 2 размера: текущее количество элементов и максимально возможное без переаллокаций.
Второй вариант - 3 указателя. Начало, конец текущих данных и конец всего стораджа.
template<typename T>
class Vector {
...
T * begin;
T * end;
T * end_of_storage;
};
И именно последний вариант реализован например в GCC.
У каждого варианта свои плюсы и минусы:
- с помощью 3-х указателей тяжело считать size(), но легко итерироваться и вычислять end().
- указатель и 2 размера легко считают size(), но у них тяжеловато с end().
И в связи с тем, что в С++26 завезли харденинг(рантайм проверки на соблюдение контрактов стандартной библиотеки(например выход за границы массива)) ситуация приобретает интересный поворот.
Харденинг неявно увеличивает количество вызовов метода size(). Поэтому становится выгоднее использовать "наивную" реализацию через указатель и 2 числа.
Ребята из гугла проверили это на своем софте(у них харденинг проверки уже давно реализованы) и получили буст вплоть до 0.6% перфа на количество обработанных запросов в секунду! Не самого вектора, а самих сервисов.
Вот статейка, кому интересно. Крутая статья, кстати. Там в деталях расписано, как внутри std:vector функционирует и еще много всякой полезнятины.
Measure your performance. Stay cool.
#STL #optimization #cpp26
Г
Грокаем C++
07.07.2026 07:04 · 👁 2.1K
Разгоняемся до предела
С++ - это про перформанс. Даже если все качественно работает, но работает медленно - этого недостаточно.
Собственно, поэтому мне так зашел летний челлендж на CodeRun — это как раз про эффективность, а не просто про «зеленые тесты».
Что по механике:
🔸 15 задач, от легких к суровым, выходят пачками по 5 штук каждые 5 дней
🔸 общий зачет + 11 отдельных лидербордов по языкам (плюсы там, естественно, есть)
🔸 стартовать можно в любой день до 17 июля
Ключевая фишка — performance engineering. Тут мало пройти по корректности. Чем больше джуса выжали из кода, тем больше баллов.
Если в литкоде ты решил задачу с оптимальным по ассимптотике алгоритмом и кайфуешь, то здесь так не прокатит. Тут начинается настоящий C++: понимание, где у тебя лишние аллокации, как подружиться с кэшем, а где можно немного компайл тайма вставить.
Что можно за это получить:
— топ-1 по каждому из 11 языков → мерч-пак CodeRun
— топ-3 общего зачета → коллекционный лего-набор (для тех, кто соскучился по ручной сборке абстракций)
— топ-200 общего зачета → скип отборочного контеста + пробное интервью в Яндекс(!)
В общем, если хочется летом не только шашлыки жарить, но и профилировщик погонять — залетай
Г
Грокаем C++
03.07.2026 09:00 · 👁 3K
Правило 0
#новичкам
Правило 5 говорит нам определять все 5 специальных методов, если нам нужно определить хотя бы 1 из них.
Но как часто вам сейчас реально нужно определять специальные методы? Возможно в каком-то библиотечном или фреймворкочном коде это встречается почаще. Но чем выше уровень абстракций в коде, тем ниже вероятность встретить определение специальных методов.
Почему?
Язык уже сейчас богат на различные средства управления ресурсами. Есть стандартные контейнеры, есть умные указатели. Они уже внутри себя определяют семантику владением ресурсом и предоставляют простой интерфейс для работы с ними.
Нам не обязательно писать все руками(если не нужно выжимать микросекунды и килобайты памяти). Для управления ресурсами можно пользоваться готовыми инструментами и сфокусироваться на логике приложения.
Именно об этом и говорит правило 0. Если вы пользуетесь обертками управления ресурсами и вас устраивает дефолтное поведение компилятора при операциях с объектами, то вам не нужно определять ни одного специального метода.
class rule_of_zero
{
std::string cppstring;
public:
rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
// std::string will manage resource
Однако иногда есть один интересный кейс.
Пускай у вас есть полиморфный класс. Да, вы там используете все возможные обертки и хотите использовать правило 0.
Но у вас не выйдет.
Как минимум вы определите виртуальный деструктор и пометите его как default. То есть уже не 0.
И только этот факт вас уже должен насторожить. Потому что могут быть скрытые проблемы. Например, может произойти случайный слайсинг полиморфного объекта при передаче по значению. Чтобы избежать неприятностей, вам нужно удалить мув и копи операции.
class Base {
public:
virtual ~Base() = default;
Base(const Base&) = delete;
Base& operator=(const Base&) = delete;
Base(Base&&) = delete;
Base& operator=(Base&&) = delete;
// ... other constructors and functions ...
};
В этом случае вы попали четко в правило 5: даже такая синтаксическая необходимость дефолтирования виртульного деструктора должно вам настрожить, подумать о последствиях и в итоге прийти к определению всех 5 методов.
Follow the rules. Stay cool.
#design #goodpractice