C++ geek
3.61K subscribers
277 photos
5 videos
28 links
Учим C/C++ на примерах
Download Telegram
C++: Заставьте компилятор работать за вас (constexpr и consteval)

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

В современном C++ мы можем «запекать» результаты функций прямо в итоговый .exe файл. Для этого есть два инструмента.

1. constexpr - «Вычисли до запуска, если сможешь» (C++11)

Ключевое слово constexpr говорит компилятору: "Если все аргументы этой функции известны заранее, вычисли её прямо сейчас. Если нет - оставь до выполнения программы (Run-time)".

Это невероятно удобно для универсальных функций.


// Функция может работать и до запуска, и во время!
constexpr int GetArea(int width, int height) {
return width * height;
}

int main() {
// 🚀 Вычислится компилятором! В код вставится просто "200".
// Zero runtime cost.
int a = GetArea(10, 20);

int w;
std::cin >> w;
// 🐢 Вычислится процессором во время работы (w неизвестно заранее).
int b = GetArea(w, 20);
}



2. consteval - «Вычисли до запуска, или умри!» (C++20)

У constexpr есть проблема: мы не всегда уверены, вычислилась ли функция компилятором, или она тихо «соскользнула» в Run-time, замедляя программу.

Поэтому в C++20 добавили consteval. Это строгий приказ (Immediate Function). Если компилятор не может выполнить функцию прямо сейчас - он выдаст ошибку компиляции.


// Обязана выполниться во время компиляции
consteval int MagicHash(std::string_view str) {
int hash = 0;
for (char c : str) hash += c;
return hash;
}

int main() {
// Отлично. Компилятор сам посчитает хэш слова "admin".
int h1 = MagicHash("admin");

std::string user_input = "test";
// ОШИБКА КОМПИЛЯЦИИ! user_input нельзя знать заранее.
int h2 = MagicHash(user_input);
}



📈 Зачем это нужно?

1. Максимальная производительность: Вы переносите время выполнения на этап сборки программы. Для пользователя всё работает за O(1).

2. Замена #define: Раньше константы и простые формулы писали через макросы препроцессора. Теперь constexpr делает это безопасно, с проверкой типов.

3. Безопасность: С consteval вы гарантируете, что тяжелая инициализация (например, генерация таблиц поиска) не ударит по производительности в продакшене.

💡Итог: Пишете математику или чистые функции без побочных эффектов? Ставьте constexpr.
Хотите 100% гарантию, что вычисления не попадут в готовый бинарник? Ставьте consteval.

#cpp #cpp20 #constexpr #optimization #performance #coding #tips

➡️ @cpp_geek
👍53
🎭 Сколько стоит virtual? Вся правда о полиморфизме и магии final

Мы обожаем интерфейсы и ООП. Добавить virtual перед методом - минутное дело, и вот наш код уже гибкий и расширяемый. Но задумывались ли вы, чем мы за это платим на уровне железа?

⚙️ Анатомия виртуального вызова (vtable)

Если в классе есть хотя бы одна виртуальная функция, компилятор втайне добавляет в каждый объект этого класса скрытое поле - vptr (указатель на виртуальную таблицу). Сама таблица (vtable) хранится где-то в памяти и содержит адреса реальных функций.

Как происходит вызов obj->DoWork() под капотом:

1. Процессор идет по адресу объекта obj.
2. Читает скрытый указатель vptr.
3. Делает прыжок в память, где лежит vtable.
4. Находит там нужный адрес функции для конкретного класса-наследника.
5. Делает еще один прыжок, чтобы выполнить код.

🚨 Почему это бьет по производительности?

Дело даже не в лишних прыжках по памяти (хотя промахи кэша процессора - это больно).
Главная проблема: Виртуальность убивает оптимизации.

Когда компилятор видит вызов виртуальной функции через указатель, он "слепнет". Он не знает, код какого именно наследника будет вызван во время работы программы (Run-time). Из-за этого он не может применить Inlining (встраивание тела функции вместо вызова) - а это самая мощная оптимизация в C++.

🛡 Спаситель из C++11: ключевое слово final

Слово final запрещает дальнейшее наследование класса или переопределение метода. Но кроме защиты архитектуры, оно делает невероятное: включает Девиртуализацию (Devirtualization).


class Base {
public:
virtual void Process() = 0;
};

// Мы жестко фиксируем класс: от него нельзя наследоваться!
class Derived final : public Base {
public:
void Process() override {
/* важная логика */
}
};

void RunOptimized(Derived* obj) {
// Компилятор видит: тип obj — Derived.
// Derived помечен как final. Значит, никто физически
// не мог переопределить метод Process!

// 🚀 МАГИЯ: Компилятор выбрасывает vtable, игнорирует vptr
// и превращает вызов в обычный, или вообще инлайнит (встраивает) его!
obj->Process();
}



💡 Золотое правило современного C++:
Относитесь к классам как к запечатанным. Пишите final для всех классов (особенно тех, что реализуют интерфейсы), если только вы не проектируете их специально для дальнейшего наследования.

Вы получите защиту от глупых архитектурных ошибок и бесплатный прирост скорости!

#cpp #cpp11 #oop #optimization #performance #coding #tips

➡️ @cpp_geek
👍10
🚦 Многопоточность без тормозов: std::atomic против std::mutex

Мы все знаем классику: если несколько потоков одновременно пишут в одну переменную, случается Data Race (гонка данных), и программа выдает мусор или падает.

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

🐢 Почему std::mutex такой медленный?
Мьютекс - это тяжеловесный механизм операционной системы.
Если Поток А захватил мьютекс, а Поток Б пытается сделать то же самое, ОС видит, что «дверь закрыта». ОС усыпляет Поток Б (происходит Context Switch) и отдает ядро процессора кому-то другому. Когда Поток А отпускает мьютекс, ОС должна снова «разбудить» Поток Б.

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

🚀 Решение: std::atomic (Lock-Free магия)
Вместо того чтобы просить ОС усыплять потоки, мы можем использовать std::atomic. Он работает на уровне самого железа (процессора).



Для атомиков компилятор генерирует специальные ассемблерные инструкции (например, с префиксом LOCK на архитектуре x86). Процессор сам на аппаратном уровне гарантирует, что инкремент произойдет неделимо (атомарно). Никаких обращений к ОС, никаких засыпаний!

🆚 Давайте сравним в коде:


// 🐢 ТЯЖЕЛОВЕСНО (std::mutex)
std::mutex mtx;
int counter = 0;

void AddMutex() {
std::lock_guard<std::mutex> lock(mtx);
counter++; // Заморозили поток ОС ради одной операции!
}



// 🚀 БЕЗ БЛОКИРОВОК (std::atomic)
std::atomic<int> counter = 0;

void AddAtomic() {
counter++; // Выполняется за наносекунды на уровне CPU
}


Разница в скорости на простых операциях типа счетчиков или флагов может достигать 50-100 раз в пользу std::atomic!

⚖️ Когда что использовать?

Нельзя просто взять и везде заменить мьютексы на атомики.
Используйте std::atomic, если вам нужно защитить только одну простую переменную (счетчик метрик, флаг остановки bool, указатель на узел в lock-free очереди).

🛑 Используйте std::mutex, если вам нужно выполнить сложную логику, защитить кусок памяти (std::vector, std::map) или обновить сразу две и более переменных одновременно.

💡 Итог: Многопоточность - это искусство компромисса. Оставляйте тяжелые замки (mutex) для больших комнат, а для маленьких сейфов (int, bool) используйте умные аппаратные ключи (atomic).

#cpp #multithreading #atomic #mutex #optimization #coding #tips

➡️ @cpp_geek
👍62
🧬 Двойная цена std::shared_ptr: Почему профи всегда пишут make_shared?

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

Встречали такой код?

// 🐢 ПЛОХО: Классический подход
std::shared_ptr<User> user(new User());


Кажется, всё логично: выделили память через new, передали в shared_ptr. Но на деле вы заставляете программу сделать две аллокации (выделения памяти) вместо одной.

⚙️ Анатомия shared_ptr

std::shared_ptr состоит из двух частей:

1. Сам объект (ваши данные User).

2. Контрольный блок (Control Block) - служебная структура, где лежат счетчики ссылок (reference count) и счетчики weak_ptr.

Когда вы пишете std::shared_ptr<User>(new User()), происходит следующее:

1. Отрабатывает new User() - программа идет к ОС и просит кусок памяти.

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

Два системных вызова. Фрагментация кучи (heap). Промахи кэша процессора, потому что объект и счетчик лежат в разных концах памяти.



🚀 Решение: std::make_shared


// 🚀 ХОРОШО: Единый блок памяти
auto user = std::make_shared<User>();


Что делает make_shared? Он считает размер вашего объекта User + размер Контрольного блока, и просит у операционной системы один большой кусок памяти за один раз.

Плюсы:
В 2 раза меньше аллокаций. Код работает быстрее.

Cache Locality. Объект и счетчик ссылок лежат в памяти впритык друг к другу. Процессор это обожает.

Безопасность. До C++17 старый подход с new мог привести к утечке памяти, если функция принимала несколько аргументов и один из них бросал исключение. С make_shared это исключено.

🦇 Темная сторона (О чем не пишут в туториалах)

Есть ровно один случай, когда make_shared может навредить. Это связано со слабыми указателями (std::weak_ptr).

Если вы удалили все shared_ptr, вызывается деструктор объекта User. Но если остался хотя бы один weak_ptr, Контрольный блок обязан жить!
А так как make_shared склеил Контрольный блок и объект в один кусок памяти, оперативная память из-под объекта User не вернется системе, пока жив weak_ptr (даже если сам объект уже "мертв" и деструктор отработал).
Если ваш объект весит 500 Мегабайт - вы получите «фантомную» утечку памяти.

💡В 99% случаев используйте std::make_shared. Используйте new std::shared_ptr только если у вас гигантские объекты, на которые подолгу смотрят «зависшие» weak_ptr, или если вам нужен кастомный удалитель (custom deleter).

#cpp #memory #pointers #optimization #sharedptr #coding #tips

➡️ @cpp_geek
👍51
♻️ Идеальное преступление: Как создать утечку памяти с помощью умных указателей?

С появлением std::shared_ptr в C++ многие выдохнули: "Наконец-то счетчик ссылок всё сделает за нас, больше никаких утечек!". Но умные указатели не обладают интеллектом. И их очень легко обмануть.

Самая частая причина «фантомных» утечек памяти в современном C++ это Циклическая зависимость (Circular Dependency).

🪤 Ловушка: Змея, кусающая себя за хвост

Представьте игру. У нас есть Игрок (Player) и Гильдия (Guild).
• Игрок должен знать, в какой Гильдии он состоит.
• Гильдия должна знать, кто её лидер (Игрок).

Вы пишете такой код:


struct Player; // Предварительное объявление

struct Guild {
std::shared_ptr<Player> leader;
~Guild() { std::cout << "Guild deleted\n"; }
};

struct Player {
std::shared_ptr<Guild> myGuild;
~Player() { std::cout << "Player deleted\n"; }
};

void Play() {
auto p = std::make_shared<Player>(); // ref_count(Player) = 1
auto g = std::make_shared<Guild>(); // ref_count(Guild) = 1

p->myGuild = g; // ref_count(Guild) = 2
g->leader = p; // ref_count(Player) = 2
}
// Конец функции. Локальные p и g уничтожаются.
// ref_count(Player) падает до 1.
// ref_count(Guild) падает до 1.


Итог: Функция завершилась. Объекты больше никому в программе не нужны. Но их деструкторы никогда не вызовутся. Они держат друг друга в заложниках, потому что счетчик не упал до нуля. Вы потеряли память.

⚔️ Спаситель: std::weak_ptr

std::weak_ptr - это умный указатель-наблюдатель. Он умеет смотреть на объект, которым владеет shared_ptr, но не увеличивает его счетчик ссылок.

Чтобы разорвать цикл, мы должны определить, кто кем владеет (кто важнее), а кто просто ссылается. Допустим, Гильдия существует независимо от лидера, поэтому она будет просто "наблюдать" за ним.

Правильный код:


struct Guild {
// Слабая ссылка! Не влияет на время жизни Player.
std::weak_ptr<Player> leader;
};


Теперь при выходе из функции счетчик Player спокойно упадет до нуля. Player удалится. Его деструктор удалит shared_ptr на Гильдию. Счетчик Гильдии упадет до нуля, и она тоже удалится. Чистая победа!

👀 Как пользоваться weak_ptr?

Так как weak_ptr не гарантирует, что объект еще жив (ведь он его не держит), из него нельзя просто так прочитать данные. Вы обязаны превратить его в shared_ptr с помощью метода .lock().


// Если Игрок еще жив, lock() вернет валидный shared_ptr.
// Если Игрок удален, lock() вернет nullptr.
if (std::shared_ptr<Player> ptr = g->leader.lock()) {
std::cout << "Лидер на месте: " << ptr->name;
} else {
std::cout << "Лидер покинул нас...";
}


💡 Золотое архитектурное правило:
Стройте связи в виде дерева.
Сверху вниз - владение (shared_ptr или unique_ptr).
Снизу вверх - наблюдение (weak_ptr или обычный *, если время жизни жестко гарантировано). Никогда не делайте цикл из shared_ptr.

#cpp #memory #smartpointers #leaks #oop #coding #tips

➡️ @cpp_geek
👍94