C++ geek
3.61K subscribers
277 photos
5 videos
28 links
Учим C/C++ на примерах
Download Telegram
🎭 Сколько стоит 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::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