← Назад к вопросам

Какие знаешь проблемы умных указателей?

2.2 Middle🔥 301 комментариев
#Умные указатели и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблемы и подвохи умных указателей

Умные указатели (smart pointers) — unique_ptr, shared_ptr, weak_ptr — это мощный инструмент для управления памятью в C++, но они имеют собственные проблемы и подводные камни, которые нужно знать.

Проблема 1: Циклические ссылки (circular references)

Это самая опасная проблема с shared_ptr — циклические ссылки приводят к утечкам памяти:

struct Node {
    int data;
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // ⚠️ Циклические ссылки!
};

// Утечка памяти
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

node1->next = node2;
node2->prev = node1;  // Циклическая ссылка!

// node1 и node2 имеют счётчики ссылок > 1
// Когда выходят из scope, не удаляются!
// Утечка: оба объекта живут вечно

Решение: использовать weak_ptr:

struct Node {
    int data;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // ✅ weak_ptr не увеличивает счётчик
};

// Теперь memory leak невозможен
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1;  // Безопасно!

Проблема 2: weak_ptr требует lock()

weak_ptr не гарантирует, что объект всё ещё существует:

std::weak_ptr<int> weak;

{
    auto strong = std::make_shared<int>(42);
    weak = strong;
}  // strong удалён

// ❌ Опасно — может быть nullptr
auto value = *weak.lock();  // Crash!

// ✅ Правильно
if (auto shared = weak.lock()) {
    std::cout << *shared;  // Безопасно
} else {
    std::cout << "Object deleted";
}

Проблема 3: Overhead памяти

Умные указатели требуют дополнительной памяти:

// unique_ptr
std::unique_ptr<int> u1(new int(42));
// Размер: обычно 8 байт (один указатель) на 64-bit системе

// shared_ptr
std::shared_ptr<int> s1(new int(42));
// Размер: 16 байт (два указателя: на объект и на control block)
// Control block содержит счётчик ссылок, deleter и прочее

// Для контейнеров это значимо
std::vector<std::shared_ptr<int>> vec(1000000);
// Дополнительно выделено: ~16 МБ на сами указатели

Проблема 4: Reference counting overhead

shared_ptr использует атомарные операции для счётчика ссылок, что может быть медленным:

class Container {
public:
    // ❌ Медленно в многопоточной программе
    void addItem(std::shared_ptr<Item> item) {
        items_.push_back(item);  // Атомарное увеличение счётчика
    }
    
    // ✅ Быстрее: move semantics
    void addItem(std::shared_ptr<Item>&& item) {
        items_.push_back(std::move(item));
    }
    
private:
    std::vector<std::shared_ptr<Item>> items_;
};

Проблема 5: Неправильное использование enable_shared_from_this

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    std::shared_ptr<MyClass> getSelf() {
        // ❌ ОПАСНО: вызов без shared_ptr
        return shared_from_this();  // Может выбросить исключение!
    }
};

int main() {
    {
        MyClass obj;  // Простой объект, не shared_ptr!
        auto self = obj.getSelf();  // Исключение: bad_weak_ptr
    }
    
    {
        auto ptr = std::make_shared<MyClass>();
        auto self = ptr->getSelf();  // ✅ Безопасно
    }
}

Проблема 6: Конверсия shared_ptr

Жёсткое наследование может привести к проблемам при конверсии:

class Base { virtual ~Base() = default; };
class Derived : public Base {};

std::shared_ptr<Base> createBase() {
    auto derived = std::make_shared<Derived>();
    return derived;  // ✅ Неявная конверсия работает
}

// Но это опасно
std::shared_ptr<Derived> derived_ptr = std::make_shared<Derived>();
std::shared_ptr<Base> base_ptr;

// ❌ Ошибка компиляции (хорошо!)
// base_ptr = derived_ptr.get();  // get() возвращает Base*

// ✅ Правильно
base_ptr = std::static_pointer_cast<Base>(derived_ptr);

Проблема 7: Deleter и custom deleters

Custom deleters могут вызвать проблемы:

// ❌ Проблема: deleter — часть типа
std::unique_ptr<int, void(*)(int*)> u1(
    new int(42),
    [](int* p) { std::cout << "Deleting\n"; delete p; }
);

std::unique_ptr<int, void(*)(int*)> u2(
    new int(43),
    [](int* p) { delete p; }  // Разный lambda = разный тип!
);

// u1 и u2 имеют разные типы, несмотря на одинаковое поведение
// Нельзя присвоить u1 = std::move(u2);

// ✅ Правильно: использовать std::function
std::unique_ptr<int, std::function<void(int*)>> u3(
    new int(44),
    [](int* p) { delete p; }
);

Проблема 8: Производительность при копировании shared_ptr

// ❌ Медленнее: копирует указатель, увеличивает счётчик
void processItem(std::shared_ptr<Item> item) {
    // item скопирован, счётчик увеличен
}

auto item = std::make_shared<Item>();
processItem(item);  // Дорого!
processItem(item);  // Дорого!

// ✅ Быстрее: используй const reference
void processItem(const std::shared_ptr<Item>& item) {
    // Просто передаём ссылку
}

processItem(item);  // Дешево!

Проблема 9: Смешивание raw pointers и smart pointers

class ResourceManager {
pubate:
    std::shared_ptr<Resource> resource_;
    
public:
    Resource* getResource() {  // ❌ ОПАСНО
        return resource_.get();
    }
};

int main() {
    {
        ResourceManager mgr;
        auto raw = mgr.getResource();
    }  // resource удалён
    
    // raw — висячий указатель!
    // raw->doSomething();  // Undefined behavior
}

Проблема 10: Чрезмерное использование shared_ptr

// ❌ Лишняя сложность
void processData(std::shared_ptr<Data> data) {
    // Нам не нужна shared семантика для параметра
    // Просто увеличиваем счётчик без причины
}

// ✅ Правильно
void processData(const Data& data) {
    // Или если нужна модификация:
}

void processData(Data& data) {
}

Проблема 11: Self-deleting объекты

// ❌ Невозможно реализовать safely
class Listener {
public:
    void onEvent() {
        // Хотим удалить себя из вектора...
        // Но находимся внутри метода, это опасно
    }
};

std::vector<std::shared_ptr<Listener>> listeners;

Решение: отсложить удаление или использовать слабые ссылки:

class Listener : public std::enable_shared_from_this<Listener> {
public:
    void onEvent() {
        // Отсложить удаление на позже
        scheduleForDeletion(shared_from_this());
    }
};

Лучшие практики

  1. Используй unique_ptr по умолчанию — проще и быстрее
  2. shared_ptr только когда нужна многократная ссылка
  3. Избегай циклических ссылок — используй weak_ptr
  4. Предпочитай const reference параметрам вместо копирования shared_ptr
  5. Не используй raw pointers для долгожизненных объектов
  6. Тестируй на утечки памяти — используй Valgrind или AddressSanitizer
  7. Документируй ownership семантику
  8. Избегай enable_shared_from_this если возможно

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

Какие знаешь проблемы умных указателей? | PrepBro