Skip to content

ООП Лекция 10. Умные указатели.

Vladislav Mansurov edited this page May 2, 2022 · 1 revision

Умные указатели

Проблема утечки памяти

Какая у нас проблема? У нас серьёзная проблема с памятью. Причём не одна, а много. Наша задача сегодня решить все эти проблемы с памятью.

Существует проблема. Предположим, у нас есть класс А, в котором есть метод f(). Страшный код. Мы не знаем, что творится внутри f(), и, естественно, мы используем механизм обработки исключительных ситуаций. Внутри f() происходит исключительная ситуация, она приводит к тому, что мы перескакиваем на какой-то обработчик, неизвестно где находящийся. Это приводит к тому, что объект p не удаляется - происходит утечка памяти.

Бандитский код:

{
	A* p = new A;
	p->f();   // Внутри f() происходит исключительная ситуация
	delete p; // Объект p не удаляется
}

Плохой код:

{
    A obj;
    obj.f();
}

Как решить эту проблему?

Идея: обернуть объект в оболочку, которая статическая распределяет память. Эта оболочка будет отвечать за этот указатель. И соответственно, поскольку мы статически распределили, когда будет вызываться деструктор, в деструкторе мы будем освобождать память.

Holder

Мы можем указатель p обернуть в какой-то объект - хранитель. Этот объект будет содержать указатель на объект A. Задача объекта: при выходе из области видимости объекта-хранителя будет вызываться деструктор obj, в котором мы можем уничтожить объект A.

{
    Holder<A> obj(new A);
} 

Для объекта хранителя достаточно определить три операции - * (получить значение по указателю), ->(обратиться к методу объекта, на который указывает указатель) и bool(проверить, указатель указывает на объект, nullptr он или нет). Чтобы можно было записать obj->f();. То есть эта оболочка должна быть "прозрачной". Её задача должна быть только вовремя освободить память, выделенную под объект. Мы работаем с объектом класса А через эту оболочку.

Краткая реализация:

template <typename T>
class Holder
{
    T* ptr{nullptr}; // Указатель на объект (сразу же его обнуляем)
public:
    explicit Holder(T *p) : ptr(p) {}; // Заxватываем указатель и запрещаем неявный вызов конструктора
    ~Holder() {delete ptr;}            // Задача деструктора - удалить объект
    
	// Определяем джентельменский набор из трёх операторов
    T& operator *() const {return *ptr;}
    T* operator ->() const {return ptr;}
    operator bool() const { return ptr != nullptr; }
    
    // Запрещаем конструктор копирования и оператор присваивания
    Holder(const Holder &) = delete; // Если у нас параметр T по умолчанию, можно его явно не указвать
                                     // а использовать & (просто пояснение)
    Holder operator=(const Holder &) = delete; 
};

Это самый простой вариант хранителя.

В чём же проблема? Время жизни может не совпадать. Висящий указатель... Жадный (украинский) подход.

Проблема висящего указателя

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

class A {...};

class B
{
	A* p;
}

Например, мы получили указатель p. Этот объект может быть удалён, и в этом случае возникает проблема: указатель, инициализированный каким-то адресом, будет указывать на удалённый объект. Можно рассматривать каждый объект, который держит указатель, как хранитель. То есть мы отдаём указатель на объект, а объект-хранитель считает, что этот объект его собственный, происходит захват.

В случае если хранитель отдаёт объект, нужно позаботиться о том, чтобы не образовался "висящий" указатель, то есть указатель на объект, которого нет.

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

Представим, что на один объект держат указатели несколько объектов. Как понять, какой из объектов должен удалять этот указатель? Если это отдавать на откуп программиста, то о надежности такого кода говорить нельзя, возможно ошибка. Допустим, мы выбрали один из объектов ответственным. Какая гарантия, что он не уничтожится раньше, чем другие два объекта?

image

Жадный подход не годится!

Идея какая: последний, кто уезжает, выключает свет. То есть последний объект (класса B), который будет уничтожаться, он должен позаботиться об объекте класса A.

До C++11 все эти проблемы пытались решить с помощью одного умного указателя.

Умные указатели

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

Существует три вида умных указателей, каждый решает свою проблемы.

unique_ptr

Пример с Holder (ранее) по существу представляет собой указатель unique_ptr. жестко сберегает какой-то один объекта. Он хранит уникальную ссылку на объект и не позволяет другим указателям владеть этим объектом.

Основная проблема: обработка исключительных ситуаций, чтобы не было утечки памяти. Когда мы чётко понимаем, что указатель будет только один (объект только один).

Операции:

  • создание
  • удаление
  • набор ->, *, bool, [] (специализация unique_ptr)

То есть, unique_ptr может владеть массивом объектов.

  • Конструктора копирования, оператора присваивания нет.

Пример реализации unique_ptr

template <typename Type>
class UniquePtr
{
public:
	UniquePtr() = default;
	constexpr UniquePtr(nullptr_t) {}
	explicit UniquePtr(Type* p) noexcept : ptr(p) {}
	UniquePtr(UniquePtr<Type>&& vright) noexcept;
	~UniquePtr()	{ delete ptr; }
	UniquePtr<Type>& operator=(nullptr_t) noexcept;
	UniquePtr<Type>& operator=(UniquePtr<Type>&& vright) noexcept;
	Type& operator*() const noexcept { return *ptr;  }
	Type* const operator->() const noexcept { return ptr; }
	explicit operator bool() const noexcept { return ptr != nullptr; }
	Type* get() const noexcept { return ptr; }
	Type* release() noexcept;
	void reset(Type* p = nullptr) noexcept;
	UniquePtr(const UniquePtr<Type>&) = delete;
	UniquePtr& operator=(const UniquePtr<Type>&) = delete;
private:
	Type* ptr{ nullptr };
};

# pragma region Method UniquePtr
template <typename Type>
UniquePtr<Type>::UniquePtr(UniquePtr<Type>&& vright) noexcept
{
	ptr = vright.ptr;
	vright.ptr = nullptr;
}

template <typename Type>
UniquePtr<Type>& UniquePtr<Type>::operator=(nullptr_t) noexcept
{
	reset();
	return *this;
}

template <typename Type>
UniquePtr<Type>& UniquePtr<Type>::operator=(UniquePtr<Type>&& vright) noexcept
{
	ptr = vright.ptr;
	vright.ptr = nullptr;
	return *this;
}

template <typename Type>
Type* UniquePtr<Type>::release() noexcept
{
	Type* p = ptr;
	ptr = nullptr;
	return p;
}

template <typename Type>
void UniquePtr<Type>::reset(Type* p) noexcept
{
	delete ptr;
	ptr = p;
}

namespace Unique
{

template <typename Type>
UniquePtr<Type> move(const UniquePtr<Type>& unique)
{
	return UniquePtr<Type>(const_cast<UniquePtr<Type>&>(unique).release());
}

}
# pragma endregion

class A
{
public:
	A() { cout << "Constructor A;" << endl; }
	~A() { cout << "Destructor A;" << endl; }
	void f() { cout << "Method f;" << endl; }
};

int main()
{
	UniquePtr<A> obj1(new A); // причём только явный вызов конструктора
	obj1->f();
	(*obj1).f();
	UniquePtr<A> obj2;
//	obj2 = obj1; Error!!!
	obj2 = Unique::move(obj1);
}
unique_ptr<A> obj(new A);

Но здесь проблема! Слово new.

Вместо new A мы можем написать make_unique<A>()

make_unique<A>() - шаблон с переменных числом параметров.

template <typename Type, typename ...Args>
unique_ptr<Type> make_unique_ptr(Args&&...args)
{
    return unique_ptr<Type>(new Type(forward<Args>(args)...));
}
...
unique_ptr<A> obj1(move(obj))
                  // или
                  (obj.release())

shared_ptr и weak_ptr

image

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

Указатель shared_ptr содержит счетчик. Если shared_ptr обеспечивает нас счетчиком, это так называемое совместное владение, то а паре с ним идет weak_ptr - слабое владение. Этот указатель не отвечает за освобождение памяти из под объекта. Он может только проверить, есть объект или его нет. Эти два указателя связаны между собой.

Идея shared_ptr и weak_ptr

У нас должен быть счетчик countS, определяющий, сколько объектов указывают на сберегаемый объект. Указателю weak_ptr тоже надо знать об этом счетчике.

Пусть есть какой-то базовый класс, от которого порождаем два класса: shared_ptr и weak_ptr. И тому и другому нужен указатель на object и нужен счетчик countS. Базовый класс содержит указатель на объект, и счетчик countS тоже должен быть доступен всем shared_ptr и weak_ptr, следовательно, счетчик countS мы тоже должны вынести, как объект.

Так как память object вынесли, по счетчику countS weak_ptrопределяет, есть ли этот object или нет. Если счетчик равен нулю, то объекта нет. Когда создается новый shared_ptr на область памяти object, счетчик countS увеличивается. Удаляется shared_ptr - счетчик уменьшается. Если счетчик равен нулю - эта память должна быть освобождена.

А что будет отвечать за память счетчика countS, когда освободится память из под object? Здесь встает необходимость считать не только количество объектов shared_ptr , но и количество объектов weak_ptr. То есть, по существу у нас не один счетчик, а два. Счетчик weak_ptr нужен для того, что если он станет равен нулю, и второй счетчик равен нулю, освободить эту память. Соответственно, общий класс для shared_ptr и weak_ptr может решать эту проблему. Он будет контролировать и память объектов, и область счетчика.

В общем, суть в том, что мы не можем удалить счетчик countS, так как он нужен weak_ptr, чтобы понять, есть объект, или нет.

Мы можем удалить счетчик только в том случае, когда количество weak_ptr + количество shared_ptr будет равняться 0.

Общая таблица умных указателей

Чтобы использовать эти указатели, надо подключить заголовочный файл <memory>.

Memory Владение Операторы Копия Методы
unique_ptr строгое *, ->, bool, [] Нет get, release, reset, swap
shared_ptr совместное *, ->, bool, [] Да get, reset, use_count, unique (true, если счётчик shared равен 1, иначе false)
weak_ptr слабое Нет Да use_count, expired (возвращает признак, есть объект или его нет), reset, lock (возвращает shared_ptr, на основе weak мы создаём shared)

Подробнее про unique_ptr.

Мы можем работать не с одним объектом, а с несколькими объектами - оператор []. Есть проблема - мы не знаем, сколько объектов хранит unique_ptr, и с помощью unique_ptr мы это определить не можем. Должно быть данное, которое (если unique_ptr хранит, например, массив объектов) мы должны тащить вместе с unique_ptr. Проблема. Решение - можно сделать еще одну обертку, передавая unique_ptr и количество элементов.

Подробнее про shared_ptr.

Появляются методы, связанные с совместным владением:

нам нужно знать количество указателей shared_ptr на объект - метод use_count. метод, который говорит, один shared_ptr или нет - unique, возвращающий true, если всего один shared_ptr и false в противном случае. Подробнее про weak_ptr. Здесь реализован такой механизм... weak_ptr - слабое владение. Если мы начнем через него работать с объектом, то возможно, что объект будет удален во время работы, а нам бы этого не хотелось. Поэтому было принято решение, что на основе weak_ptr будет создаваться shared_ptr, который будет захватывать объект, увеличивать счетчик. Когда нам нужно поработать с объектом, на который указывает weak_ptr, то на его основе создаем shared_ptr с помощью метода lock, работаем и удаляем shared_ptr. Непосредственного доступа к объекту через weak_ptr нет.

Метод expired говорит, есть объект, или нет (у нас же слабое владение). Проверяется счетчик countS для shared_ptr, если он равен 0, то есть указателей shared_ptr на объект нет, возвращается true, иначе false.

Примеры реализаций shared_ptr и weak_ptr

template <typename Type>
class WeakPtr;

struct Count
{
    long countS{ 0 };
    long countW{ 0 };
    Count(long cS = 1, long cW = 0) noexcept : countS(cS), countW(cW) {}
};

template <typename Type>
class Pointers
{
public:
    long use_count() const noexcept { return rep ? rep->countS : 0; }
    Pointers(const Pointers<Type>&) = delete;
    Pointers<Type>& operator=(const Pointers<Type>&) = delete;
protected:
    Pointers() = default;
    Type* get() const noexcept { return ptr; }
    void set(Type* p, Count* r) noexcept { ptr = p; rep = r; }
    void delShared() noexcept;
    void delWeak() noexcept;
    void delCount() noexcept;
    bool _compare(const Pointers<Type>& right) const noexcept { return this->get() == right.get(); }
    void _swap(Pointers<Type>& right) noexcept
    {
        std::swap(ptr, right.ptr);
        std::swap(rep, right.rep);
    }
    void _copyShared(const Pointers<Type>& right) noexcept;
    void _copyWeak(const Pointers<Type>& right) noexcept;
    void _move(Pointers<Type>& right) noexcept;
private:
    Type* ptr{ nullptr };
    Count* rep{ nullptr };
};

# pragma region Method Pointers
template <typename Type>
void Pointers<Type>::delShared() noexcept
{
    if (!ptr) return;
    (rep->countS)--;
    if (!rep->countS)
    {
        delete ptr;
        ptr = nullptr;
        delCount();
    }
}

template <typename Type>
void Pointers<Type>::delWeak() noexcept
{
    if (rep)
    {
        (rep->countW)--;
        delCount();
    }
}

template <typename Type>
void Pointers<Type>::delCount() noexcept
{
        if (!rep->countS && !rep->countW)
        {
            delete rep;
            rep = nullptr;
        }
}

template <typename Type>
void Pointers<Type>::_copyShared(const Pointers<Type>& right) noexcept
{
    if (right.ptr)
        (right.rep->countS)++;
    ptr = right.ptr;
    rep = right.rep;
}

template <typename Type>
void Pointers<Type>::_copyWeak(const Pointers<Type>& right) noexcept
{
    if (right.rep)
        (right.rep->countW)++;
    ptr = right.ptr;
    rep = right.rep;
}

template <typename Type>
void Pointers<Type>::_move(Pointers<Type>& right) noexcept
{
    ptr = right.ptr;
    rep = right.rep;
    right.ptr = nullptr;
    right.rep = nullptr;
}
# pragma endregion

template <typename Type>
class SharedPtr : public Pointers<Type>
{
public:
    SharedPtr() = default;
    constexpr SharedPtr(nullptr_t) noexcept {}
    explicit SharedPtr(Type* p);
    SharedPtr(const SharedPtr<Type>& other) noexcept;
    explicit SharedPtr(const WeakPtr<Type>& other) noexcept;
    SharedPtr(SharedPtr<Type>&& right) noexcept;
    SharedPtr(UniquePtr<Type>&& right);
    ~SharedPtr();
    SharedPtr<Type>& operator=(const SharedPtr<Type>& vright) noexcept;
    SharedPtr<Type>& operator=(SharedPtr<Type>&& vright) noexcept;
    SharedPtr<Type>& operator=(UniquePtr<Type>&& vright);
    Type& operator*() const noexcept { return *this->get(); }
    Type* operator->() const noexcept { return this->get(); }
    explicit operator bool() const noexcept { return this->get() != nullptr; }
    bool unique() const noexcept { return this->use_count() == 1; }
    void swap(SharedPtr<Type>& right) noexcept { this->_swap(right); }
    void reset(Type* p = nullptr) noexcept { (p ? SharedPtr(p) : SharedPtr()).swap(*this); }
};

# pragma region Methods SharedPtr
template <typename Type>
SharedPtr<Type>::SharedPtr(Type* p)
{
    this->set(p, new Count());
}

template <typename Type>
SharedPtr<Type>::SharedPtr(const SharedPtr<Type>& other) noexcept
{
    this->_copyShared(other);
}

template <typename Type>
SharedPtr<Type>::SharedPtr(const WeakPtr<Type>& other) noexcept
{
    this->_copyShared(other);
}

template <typename Type>
SharedPtr<Type>::SharedPtr(SharedPtr<Type>&& right) noexcept
{
    this->_move(right);
}

template <typename Type>
SharedPtr<Type>::SharedPtr(UniquePtr<Type>&& vright)
{
    Type* p = vright.release();
    if (p)
        this->set(p, new Count());
}

template <typename Type>
SharedPtr<Type>::~SharedPtr()
{
    this->delShared();
}

template <typename Type>
SharedPtr<Type>& SharedPtr<Type>::operator=(const SharedPtr<Type>& vright) noexcept
{
    if (this->_compare(vright)) return *this;
    this->delShared();
    this->_copyShared(vright);
    return *this;
}

template <typename Type>
SharedPtr<Type>& SharedPtr<Type>::operator=(SharedPtr<Type>&& vright) noexcept
{
    if (this->_compare(vright)) return *this;
    this->delShared();
    this->_move(vright);
    return *this;
}

template <typename Type>
SharedPtr<Type>& SharedPtr<Type>::operator=(UniquePtr<Type>&& vright)
{
    this->delShared();
    Type* p = vright.release();
    this->set(p, p ? new Count() : nullptr);
    return *this;
}
# pragma endregion

template <typename Type>
class WeakPtr : public Pointers<Type>
{
public:
    WeakPtr() = default;
    WeakPtr(const WeakPtr<Type>& other) noexcept;
    WeakPtr(const SharedPtr<Type>& other) noexcept;
    WeakPtr(WeakPtr<Type>&& other) noexcept;
    ~WeakPtr();
    WeakPtr<Type>& operator=(const WeakPtr<Type>& vright) noexcept;
    WeakPtr<Type>& operator=(const SharedPtr<Type>& vright) noexcept;
    WeakPtr<Type>& operator=(WeakPtr<Type>&& vright) noexcept;
    void reset() noexcept { WeakPtr().swap(*this); }
    void swap(WeakPtr<Type>& other) noexcept { this->_swap(other); }
    bool expired() const noexcept {	return this->use_count() == 0; }
    SharedPtr<Type> lock()const noexcept { return SharedPtr<Type>(*this); }
};

# pragma region Methods WeakPtr
template <typename Type> 
WeakPtr<Type>::WeakPtr(const WeakPtr<Type>& other) noexcept
{
    this->_copyWeak(other);
}

template <typename Type>
WeakPtr<Type>::WeakPtr(const SharedPtr<Type>& other) noexcept
{
    this->_copyWeak(other);
}

template <typename Type>
WeakPtr<Type>::WeakPtr(WeakPtr<Type>&& other) noexcept
{
    this->_move(other);
}

template <typename Type>
WeakPtr<Type>::~WeakPtr()
{
    this->delWeak();
}

template <typename Type>
WeakPtr<Type>& WeakPtr<Type>::operator=(const WeakPtr<Type>& vright) noexcept
{
    if (this->_compare(vright)) return *this;
    this->delWeak();
    this->_copyWeak(vright);
    return *this;
}

template <typename Type>
WeakPtr<Type>& WeakPtr<Type>::operator=(const SharedPtr<Type>& vright) noexcept
{
    if (this->_compare(vright)) return *this;
    this->delWeak();
    this->_copyWeak(vright);
    return *this;
}

template <typename Type>
WeakPtr<Type>& WeakPtr<Type>::operator=(WeakPtr<Type>&& vright) noexcept
{
    if (this->_compare(vright)) return *this;
    this->delWeak();
    this->_move(vright);
    return *this;
}
# pragma endregion

class A
{
public:
    A() { cout << "Constructor A;" << endl; }
    ~A() { cout << "Destructor A;" << endl; }
    void f() { cout << "Method f;" << endl; }
};

int main()
{
    SharedPtr<A> obj1(new A);
    obj1->f();
    SharedPtr<A> s1, s2(obj1), s3;
    s2->f();
    cout << s2.use_count() << endl;
    WeakPtr<A> w1 = s2;
    s1 = w1.lock();
    SharedPtr<A> s4(w1);
    cout << s2.use_count() << endl;
    WeakPtr<A> w2;
    {
        SharedPtr<A> obj2(new A);
        w2 = obj2;
        if (!w2.expired())
            (w2.lock())->f();
    }
    if (!w2.expired())
        (w2.lock())->f();
    s2.reset();
    s3 = s1;
}
Clone this wiki locally