avatar
Грокаем C++
@grokaemcpp
21.07.2026 09:00
​​Ответ на квиз
#новичкам

Напомню код:

std::string makeText()
{
std::string s = "Hello World!";
return s;
}
 
int main()
{
std::string t = makeText();
std::cout << t << endl;
}


В С++ все по классике зависит от кучи деталей. Но в первую очередь от версии стандарта и компилятора. Поэтому в теории, ответом могут быть и 0, и 1, и 3. Но давайте сузим спектр обсуждений до С++23 и gcc.

Во времена, когда я только начинал изучать C++, мой ответ был бы «3»: один раз для начального выделения памяти под строку s, затем ещё два — для возврата по значению: сначала копирование-инициализация временного объекта из s, затем копирование-инициализация t из временного объекта. Каждое копирование должно триггерить аллокацию нового буфера.

Потом я узнал об оптимизации copy elision(с С++17 есть в стандарте) и конкретно NRVO и понял, что при возврате по значению умный компилятор скорее всего избежит создания трёх объектов, и сразу создаст объект с нужной строкой в main. После этого мой ответ был бы уже 1.

Но это ещё не всё. От std::string не требуется выделять память в куче (или любую другую память, о которой знает его аллокатор). Требуется лишь, чтобы он мог тем или иным способом хранить строку символов произвольной длины. Действительно, в общем случае это подразумевает выделение памяти в какой-то момент. Но наш случай не общий вообще-то. Текст "Hello World!" довольно короткий. Это 12 символов; 13, если считать завершающий нулевой символ. Типичная реализация std::string требует трёх указателей для полноценного функционирования (примерно также как мы рассказали тут для std::vector). Если на 64-разрядной машине размер указателя равен 8 символам, то наш текст занимает меньше, чем два указателя. Текст достаточно мал, чтобы поместиться внутрь объекта, память для которого выделена на стеке. Это идеальный кандидат для оптимизации малого буфера (small buffer optimization, SBO).

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

Вообще говоря, даже если не будут работать оптимизации copy elision, аллокаций все равно не будет, потому что для копирование маленькой строки нужно всего лишь данные со стека скопировать.

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

Run faster. Stay cool.

#memory #optimization
14
👍 11
🔥 6
16 12 1.3K
avatar
Грокаем C++
@grokaemcpp
20.07.2026 10:00
Квиз
#новичкам

Очень простой #quiz для по С++, который открывает ящик пандоры и довольно сложные концепции в том, как все под капотом реализовано.

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

std::string makeText()
{
std::string s = "Hello World!";
return s;
}
 
int main()
{
std::string t = makeText();
std::cout << t << endl;
}


std::string - это стандартный контейнер, который может хранить динамически изменяемые строки почти любого размера. Делает он это примерно как std::vector - без динамических аллокаций тут не обойтись.

Что мы знаем про динамические аллокации? Их любят и ненавидят. Любят за то, что позволяют делать строить сложные динамические изменяемые структуры. Ненавядят за то, что они чертовски медленные. Поэтому лучше их избегать.

Чтобы понять, как и куда бежать, надо понять, где мы сейчас находимся. Так вот вопрос: сколько динамических аллокаций будет в коде выше?

Run faster. Stay cool.
🔥 8
4
👍 3
13 9 1.7K
avatar
Грокаем C++
@grokaemcpp
20.07.2026 07:00
😭 Многие разработчики с опытом 1–5 лет сталкиваются с одной проблемой: код писать получается уверенно, но переход на Middle+ или Senior так и не происходит. Часто причина не в знании C++, а в понимании архитектуры и проектирования.
Пока приложение небольшое, изменения вносятся быстро. Но со временем новые функции начинают ломать старые, зависимости множатся, а любое изменение требует всё больше времени и проверок.

📆 22 июля в 20:00 МСК приглашаем на открытый урок, где разберём подходы, которые помогают сохранять управляемость проекта по мере его роста и обсудим, какие инженерные навыки помогают выйти на следующий профессиональный уровень.
Поговорим о цикле обработки событий, модульности, проектировании интерфейсов, взаимодействии компонентов и разделении данных, логики и представления. Вы увидите, как создавать приложения, которые проще развивать, расширять и сопровождать.

⚡️ Открытый урок проходит в преддверии старта курса «C++-разработчик. Продвинутый уровень». Вы узнаете, как архитектура, паттерны проектирования, современный C++ и практики промышленной разработки помогают расти в профессии. Зарегистрируйтесь, чтобы задать вопросы эксперту и понять, каких навыков вам не хватает для следующего карьерного шага: https://otus.pw/eFpMF/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
👍 4
🔥 3
🤣 3
2
3 1.7K
avatar
Грокаем C++
@grokaemcpp
18.07.2026 09:00
​​Сколько процессов можно запустить на одной машине?
#опытным

Симметричный вопрос относительно предыдущего поста, который затрагивает важные различия ОС.

В windows довольно простой ответ.

Жестких лимитов на количество процессов нет. Разве что PID - это 32-битное число, поэтому больше физически создать нельзя.

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

С линукс чуть интереснее. В его ядре вообще нет процессов per se. Есть задачи.

Ядро Linux имеет параметр kernel.pid_max, который определяет максимально возможный идентификатор задачи. Исторически значение по умолчанию было 65535, но в современных ядрах оно может быть значительно выше — до нескольких миллионов.

Плюс для каждого пользователя действует ограничение nproc(number of processes). Его можно посмотреть командой ulimit -u:

👉🏿 По умолчанию это значение часто составляет 1024

👉🏿 Ограничение бывает "мягким" (soft) и "жестким" (hard). Пользователь может увеличить мягкий лимит, но не выше жесткого.

👉🏿 Эти лимиты настраиваются в файлах /etc/security/limits.conf или /etc/security/limits.d/.

Допустим, у вас лимит в 100 задач. Вы можете запустить 50 процессов, в каждом из которых по 1 потоку. Или 1 процесс и 99 потоков внутри него. Главное, чтобы сумма не превысила 100.

Ну и рассуждения про ограничения ресурсов машины здесь тоже имеют место быть.

Scale up. Stay cool.

#concurrency #OS
18
😁 10
🔥 5
👍 4
❤‍🔥 2
17 1.9K
avatar
Грокаем C++
@grokaemcpp
17.07.2026 17:00
​​Сколько потоков можно запустить на одной машине?
#опытным

Из всех утюгов говорят про асинхронность вычислений и экономию ресурсов ОС. Зачем это нужно? И почему я не могу на каждую задачу запускать отдельный поток? Есть ли вообще какой-нибудь предел по количеству потоков?

Давайте раззззбираться.

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

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

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

И ограничено оно количеством вычислительных юнитов процессора. Максимальное число реально параллельных потоков можно узнать с помощью std::thread::hardware_concurrency().

Есть кстати технология HyperThreading от Intel, которая позволяет на одном физическом ядре получить 2 логических, увеличивая таким образом число потенциально параллельных потоков в 2 раза. Ускорения в 2 раза конечно не будет, но тем не менее.

"Ну хорошо. Не все 100500 потоков, которые я создал, исполняются параллельно. Не всегда нужно параллельное одновременное исполнение всех тредов. Почему всем не нравится много потоков?

Поток - это ресурс, который занимает место и им нужно управлять. Как минимум под каждый поток выделяется свой стек. На десктопах это 1-8 МБ. Поделите количество оперативной памяти на размер стека и получите максимально возможное количество потоков, которое в принципе может поместиться в память. В реальности количество будет еще меньше.

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

Ну и в некоторых системах явно прописано ограничение на количество потоков. В линуксе, например, можно увидеть заветную числеку с помощью команды cat /proc/sys/kernel/threads-max.

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

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

Scale up. Stay cool.

#concurrency #OS
👍 22
9
🔥 6
❤‍🔥 1
😁 1
7 21 1.8K
avatar
Грокаем C++
@grokaemcpp
17.07.2026 14:00
1 августа Яндекс приглашает разработчиков на бэкенд-конференцию Back to Back

Мероприятие пройдёт офлайн и онлайн в Москве, Белграде и Ереване. Обсудим производительность, системную разработку и инженерные задачи на C++, а еще архитектуру и реальные продакшен-задачи.

Часть программы в Москве:

Как устроена архитектура аутентификатора в гетерогенной системе с экспертом по разработке ПО YADRO Даниилом Подольским.

— Проектирование безопасного механизма отмены для C++20-корутин вместе со старшим разработчиком Яндекса Раедом Романовым.

—Доклад о том, что должен знать С++ программист про ABI с Константином Владимировым и Елизаветой Носковой из Syntacore.

У каждого города — своя программа. Если собираетесь посетить ивент в Москве, то сможете посетить экспертные сессии 1:1 c разбором личных и карьерных запросов и выступление питерской группы «Научно-технический рэп».

Регистрируемся тут.
8
🔥 7
👍 5
🤣 5
❤‍🔥 1
15 2K
avatar
Грокаем C++
@grokaemcpp
14.07.2026 09:00
​​Зачем нужен деструктор?
#новичкам

На самом деле даже разработчики с опытом не всегда правильно отвечают на вопрос. Потому что все эти конструкторы и деструкторы крутятся вокруг ресурсов. Но вот каких именно? Давайте разбираться.

Несколько раз слышал ответ: "освобождает память, занятую объектом".

Следом идет вопрос: - "То есть деструктор деаллоцирует память, на месте которой находится объект?". После положительного ответа я привожу пару примеров:

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
32
👍 9
🔥 4
7 16 2.6K
avatar
Грокаем C++
@grokaemcpp
13.07.2026 10:00
​​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
14
👍 12
🔥 9
❤‍🔥 1
4 18 2.3K
avatar
Грокаем C++
@grokaemcpp
13.07.2026 07:00
👨‍💻На работе код читают чаще, чем пишут. Когда смысл приходится восстанавливать по комментариям, неочевидным параметрам и скрытым договорённостям, команда теряет время, а риск ошибок растёт.

📆16 июля в 20:00 МСК на открытом уроке разберем, как переносить смысл программы в типы, сигнатуры и структуру кода. На примерах из реальных проектов участники сделают сигнатуры понятнее с помощью std::optional и enum class, упростят управление ресурсами через RAII и заменят сложные циклы готовыми алгоритмами, ranges и views.

В результате у вас появится набор приёмов для более ясного и самодокументируемого кода.

🏁Урок проходит в преддверии старта курса «C++-разработчик». Зарегистрируйтесь, чтобы познакомиться с форматом обучения, задать вопросы эксперту и потренироваться выражать намерения средствами языка, а не комментариями: https://otus.pw/y5Qj/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
8
🔥 5
👍 4
👎 2
1 7 2.1K
avatar
Грокаем C++
@grokaemcpp
09.07.2026 09:00
Когда компилятор не сгенерирует 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
👍 14
8
🔥 7
6 17 2.8K
avatar
Грокаем C++
@grokaemcpp
08.07.2026 10:00
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
30
🔥 12
👍 7
20 28 2.4K
avatar
Грокаем C++
@grokaemcpp
07.07.2026 10:05
Такие разные векторы
#опытным

Про 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
27
👍 8
🔥 7
🤔 1
5 23 2.6K
avatar
Грокаем C++
@grokaemcpp
07.07.2026 07:04
Разгоняемся до предела

С++ - это про перформанс. Даже если все качественно работает, но работает медленно - этого недостаточно.

Собственно, поэтому мне так зашел летний челлендж на CodeRun — это как раз про эффективность, а не просто про «зеленые тесты».

Что по механике:
🔸 15 задач, от легких к суровым, выходят пачками по 5 штук каждые 5 дней
🔸 общий зачет + 11 отдельных лидербордов по языкам (плюсы там, естественно, есть)
🔸 стартовать можно в любой день до 17 июля

Ключевая фишка — performance engineering. Тут мало пройти по корректности. Чем больше джуса выжали из кода, тем больше баллов.

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

Что можно за это получить:
— топ-1 по каждому из 11 языков → мерч-пак CodeRun
— топ-3 общего зачета → коллекционный лего-набор (для тех, кто соскучился по ручной сборке абстракций)
— топ-200 общего зачета → скип отборочного контеста + пробное интервью в Яндекс(!)

В общем, если хочется летом не только шашлыки жарить, но и профилировщик погонять — залетай
👍 12
8
🔥 5
👎 1
😁 1
1 24 2.3K
avatar
Грокаем C++
@grokaemcpp
03.07.2026 09:00
Правило 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
21
👍 15
🔥 8
1 19 3.4K
avatar
Грокаем C++
@grokaemcpp
02.07.2026 09:30
Правило 5
#новичкам

Так много разных способов определять специальные методы класса, что голова кругом. Помечать default, самому определять, доверяться компилятору, удалять, использовать обертки и тд.

Сложно, в общем.

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

В С++ тоже есть парочка таких правил. И начнем с самого узнаваемого.

Правило 5

Оно говорит о том, что если вы сами определили или удалили хотя бы один из 5 специальных методов класса, то остальные вам тоже нужно определить(удалить) самим.

Так стоп. Почему 5, если специальных методов класса 6?

Ответ напрямую проистекает из идеи, зачем это правило вообще нужно.

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

Допустим вы зачем-то хотите хранить строку в объекте. И не как нормальные люди, а вот нетривиально, как указатель:

class PString {
std::string* ptr;
public:
PString (const std::string& str) : ptr(new std::string(str)) {}
};


Там где new, обязательно должен быть delete:

class PString {
std::string* ptr;
public:
PString (const std::string& str) : ptr(new std::string(str)) {}
~PString () {delete ptr;}
};


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

Ну и быстро допишем все:

class PString {
std::string* ptr_ = nullptr;
public:
PString (const std::string& str) : ptr_(new std::string(str)) {}
~PString () {delete ptr_;}
PString (const PString& other) : ptr_(new std::string(*other.ptr_)) {}
PString (PString&& other) noexcept : ptr_(other.ptr_) {ptr_ = nullptr;}
PString& operator=(const PString& other) {
if (this != &other) {
delete ptr_; // free existing resource
ptr_ = new std::string(*other.ptr_); // deep copy
}
return *this;
}
PString& operator=(PString&& x) noexcept {
if (this != &x) {
delete ptr_; // free existing resource
ptr_ = x.ptr_; // move resource and then just like in move constructor
x.ptr_ = nullptr;
}
return *this;
}
};


Мы определили деструктор, копи и мув конструкторы, а также операторы копи и мув присваивания.

Где же конструктор по-умолчанию?

1️⃣ Он не управляет ресурсами, он лишь даем им значение по умолчанию.

2️⃣ Не всегда он даже нужен, а иногда даже вреден
Что будет, если я создам кольцевой буфер по-умолчанию и попытаюсь в него запихать элемент? Будет не то же самое, что и с std::vector, не выделится новая память. В худшем случае будет запись в невыделенную память. В лучшем - будет написана проверка на дурака.
Но зачем эта проверка, когда можно просто не писать конструктор по-умолчанию и избежать ошибок?

То есть для полноценного управления ресурсами достаточно копи/мув операторов, копи/мув конструкторов и деструктора.

Follow the rules. Stay cool.

#design #goodpractice
👍 13
11
🔥 7
14 18 3K
avatar
Грокаем C++
@grokaemcpp
30.06.2026 09:00
​​Специальные методы классов
#новичкам

Помимо обычных методов, которые позволяют классу выполнить какую-то полезную работу, существуют методы, которые менеджерят самими объектами. Среди них выделяют так называемые "специальные" методы классов, которые компилятор неявно за вас сгенерирует. Давайте перечислим их и поясним, почему компилятор способен вообще для них написать код:

1️⃣ Конструктор по-умолчанию. Дефолтный конструктор без аргументов. Мы уже подробно разбирали его в предыдущих постах, останавливаться не будем.

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

2️⃣ Деструктор. Он семантически разрушает объект. Забирает у него память, закрывает дескрипторы и соединения, высасывает жизнь, отбирает квартиру, машину, жену и собаку и передает это государству операционной системе.

Как так получается, что компилятор вообще способен сам сгенерировать код деструктора объекта, который владеет ресурсами?

Все благодаря RAII и способности деструктора вызывать деструкторы своих полей. Если в вашем классе Foo содержится std::vector или std::unique_ptr, вам не нужно задумываться о менеджменте памяти, авторы этих классов уже все придумали за вас. Вам нужно лишь вызывать их деструктор, что деструктор Foo делает автоматически.

using File = std::unique_ptr<FILE, decltype(&std::fclose)>;

struct Foo {
std::vector<int> numbers;
File file;
// no explicit destructor ~Foo() {}
};

void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}, {std::fopen("example.txt", "r"), &std::fclose}};
} // destroy obj here


3️⃣ Копирующий конструктор
4️⃣ Перемещающий конструктор

Для этой пары конструкторов правило единое: самое простое решение для копирования/перемещения объекта - это просто скопировать/переместить все его поля в другой объект. Компилятор с этим вполне справится сам: вызовет пачку нужных конструкторов да и все.

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

struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo(const Foo& other) {...}
// Foo(Foo&& other) {...}
};

void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo copied_in_obj = obj; // copy obj
Foo moved_in_obj = std::move(obj); // move from obj


5️⃣ Копирующий оператор присваивания
6️⃣ Перемещающий оператор присваивания

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

И вот здесь интересная комбинация.

Чтобы присвоить значение, нужно уметь разрушать текущее значение и передавать другое значение. То есть автоматическая генерация присваивания возможно благодаря тому, что компилятор сам умеет разрушать объект и копировать/присваивать значения полей.

Чуть остановимся на синтаксисе:

Foo a, b;
a = b; // copy assign
a = std::move(b); // move assign

Foo a1 = b; // THIS IS NOT AN ASSIGN


Последняя операция не является присваиванием - это вызов конструктора(в этом случае копирующего). Если объект только создается, то это всегда вызов конструктора.

Теперь примерчик:

struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo& operator=(const Foo& other) {...}
// Foo& operator=(Foo&& other) {...}
};

void foo() {
Foo forward = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo backward = {{9, 8, 7, 6, 5, 4, 3, 2, 1}};
Foo obj; // created obj here
obj = forward; // copy assign forward to obj
obj = std::move(backward); // move assign backward to obj


Итого: всего 6 специальных методов класса. Не больше, не меньше. На собесах любят про это спрашивать, так что забирайте.

Be special. Stay cool.

#cppcore #cpp11 #interview
🔥 12
8
👍 7
8 21 3K
avatar
Грокаем C++
@grokaemcpp
29.06.2026 10:00
​​Потоки и линукс
#опытным

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

И сегодня хочется поговорить об особенностях конкретно линукса.

В Linux нет потоков.

Нет системного вызова thread_create().

Никогда не было.

Когда вы вызываете pthread_create или конструктор std::thread, Linux не создаёт «поток». Он создаёт задачу.

Такую же задачу, как когда вызывается сискол fork. Отличие только в разделяемых ресурсах.

И ядро оперирует только этими задачами. Оно не разделяет потоки и процессы. Эти понятия описывают разные полюса одного понятия "задача".

Есть сискол, который создает дочернюю задачу - clone. Так вот параметры этого системного вызова и определяют ее дальнейшую судьбу.

Вызов clone без параметров поведенчески эквивалентен вызову fork: вы получаете новый полностью независимый процесс со своим виртуальным пространством, дескрипторами и тд. Но передайте в параметры набор флагов CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD и получите процесс, который по своей сути POSIX поток, который шарит эти ресурсы со своей родительской задачей. Такие процессы называют легковесные процессы или LWP.

Повторюсь, единственное отличие - различный набор разделяемых ресурсов.

Кто хочет чуть подробнее про все это почитать - вот хорошая статейка. Правда вам будут нужны заветные 3 буквы.

Know the truth. Stay cool.

#OS #concurrency
👍 26
9
🔥 4
14 35 2.7K
avatar
Грокаем C++
@grokaemcpp
27.06.2026 12:00
​​Дефолтный конструктор. Ограничения
#новичкам

Мы уже краем уха задевали эту тему, но сегодня поговорим основательно. Вот все причины, почему дефолтный конструктор не может неявно сгенерироваться компилятором:

1️⃣ У класса есть другие конструкторы

class ParametrizedConstructor {
int total;
public:
ParametrizedConstructor(int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
Example2 ex (100); // ok
Example2 ex1; // error: no default constructor


Даже если вы сами определили copy/move конструктор руками, конструктор по-умолчанию неявно генерироваться не будет:

struct UserCopyConstructor
{
UserCopyConstructor(const UserCopyConstructor&) {}
// UserCopyConstructor::UserCopyConstructor() is implicitly defined as deleted
};

struct UserMoveConstructor
{
UserMoveConstructor(UserMoveConstructor&&) = default;
// UserMoveConstructor::UserMoveConstructor() is implicitly defined as deleted
};


2️⃣ В классе есть нестатическое поле-ссылка.

Ссылка обязательно должна быть инициализирована объектом, а это невозможно сделать, не имея объект.

struct HasReference {
int& ref; // error: reference needs to be initialized
};


3️⃣ В классе есть нестатическое константное поле с тривиальным конструктором по-умолчанию. Тривиальные конструкторы по сути вообще ничего не делают, кроме как начинают лайфтайм объекта. То есть все поля заполняются мусором.

Но для константных объектов подразумевается наличие определенного постоянного значения. Мусорные значения не удовлетворяют этому требованию.

struct ConstTrivial {
const int x; // int has a trivial default constructor, so cannot default construct an object
};


Тем не менее, компилятор спокойно может проглотить константный член с нетривиальным дефолтным конструктором. Считается, что такой конструктор корректно инициализирует объект:

struct ConstNonTrivial {
const std::string s; // std::string has non-trivial constructor
};
ConstNonTrivial cnt; // OK, s is just empty string


4️⃣ Если какой-то член класса или базовый класс имеет недоступный (private) или удалённый (= delete) конструктор по умолчанию

struct NoDefault {
private:
NoDefault() {} // private constructor
};
struct Derived : NoDefault {
// Derived() is implicitly deleted
};


struct DeletedDefault {
DeletedDefault() = delete; // explicilty deleted
};
struct HasDeleted {
DeletedDefault dd;
};
HasDeleted hd; // ERROR! dd has explicilty deleted default constructor


Если знаете еще способы, пишите в комментах.

Don't be trivial. Stay cool.

#cppcore
14
🔥 7
👍 3
😁 2
❤‍🔥 1
18 15 3K
avatar
Грокаем C++
@grokaemcpp
24.06.2026 09:00
Дефолтные конструкторы. Такие разные
#опытным

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

1️⃣ Тривиальный дефолтный конструктор

По сути задача дефолтного конструктора - вызывать у своих полей и подклассов дефолтный конструктор. А что если все поля класса имеют тривиальные типы?

struct Foo {
int i;
double d;
char c;
};


На самом деле у всех тривиальных базовых типов в С++ есть конструктор по-умолчанию. Но он тривиальный. То есть ничего вообще не делает и никак не инициализирует объект.

И это свойство может передастся конструктору Foo. Если для такого типа дефолтный конструктор будет сгенерирован компилятором, то он тоже ничего делать не будет.

Конструктор по-умолчанию называется тривиальным, если:

👉🏿 У класса нет виртуальных методов.
👉🏿 Все базы класса имеют тривиальные конструкторы по-умолчанию.
👉🏿 Все поля класса имеют тривиальные конструкторы по-умолчанию.

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

2️⃣ Нетривиальный дефолтный конструктор, сгенерированный компилятором

Если ваш класс содержит поле, у которого есть нетривиальный конструктор, то вам скорее всего и не нужна С-совместимость. Но вы можете приобрести кое-что другое.

Предоставив компилятору честь сгенерировать конструктор, вы разрешаете инициализировать объект с помощью агрегатной инициализации:

struct Foo {
// Foo() = default; так тоже можно
int i;
std::string s;
std::vector<int> v;
};

Foo f = {42, "Hello World", {1, 2, 3}};


можете даже designated initialization воспользоваться:

Foo f = {.i = 42, .s = "Hello World", .v = {1, 2, 3}};


Там есть конечно еще несколько требований, но опустим их, чтобы не сбивать фокус.

3️⃣ Нетривиальный пользовательский конструктор по-умолчанию

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

struct Foo {
Foo() {};
int i;
std::string s;
std::vector<int> v;
};

Foo f = {42, "Hello World", {1, 2, 3}}; // ERROR



Да, с инициализацией у С++ довольно сложные отношения, но много чего идет от наследия С и обратной совместимости.

Don't be trivial. Stay cool.

#cppcore
👍 17
14
🔥 6
❤‍🔥 1
11 16 3.3K

Грокаем C++

9.3K
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Открыть в Telegram