Ночные эксперименты с компилятором

Умный указатель

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

Сегодня в планах поговорить про умный указатель, один самый простой его вид — unique_ptr.

Вообще умный указатель — это обертка над указателем обычным. Для чего понадобилась эта обертка?

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

Иными словами, когда вы делаете someType *p = new someType(); вы берете на себя ответственность, что где-то дальше в своем коде вы обязательно вызовете delete p;

Казалось бы пустяк. Многие думают про себя, сто пудов удалю за собой. Или — «честно-честно удалю потом». И забывают. Как можно забыть, спросите вы? Ну на самом деле очень просто и вправду забыть удалить объект. В коде может быть много ветвлений и в каких-то из ветвей объект может удаляться, в каких-то нет.

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

Но! Что если объект создан частично? Т.е. конструктор отработал неполностью, объект не создан полностью, но часть памяти под объект уже выделена. В этом случае, такая область памяти останется потерянной. Поскольку мы запросили ее у системы и… потеряли на нее указатель. Освободить мы ее теперь не сможем, поскольку указатель потерян.

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

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

Давайте попробуем реализовать свою обертку над обычным указателем. Чтобы память по нему тоже освобождалась сама. Реализация максимально проста:

#ifndef _COOL_PTR_H_
#define _COOL_PTR_H_

#include <iostream>
using namespace std;

template <class T>
class cool_ptr {

private:
    T *ptr;
    
public:
    cool_ptr(T *p);
    ~cool_ptr();
    T* get() const { return ptr; }
};

template<class T>
cool_ptr<T>::cool_ptr(T *p)
{
    ptr = p;
    cout << "Construct" << endl;
}

template<class T>
cool_ptr<T>::~cool_ptr()
{
    delete ptr;
    cout << "Delete" << endl;
}
#endif

Попробуем воспользоваться нашей оберткой. Где-то в коде:

#include "cool.h"

void test()
{
    cool_ptr <int> p(new int(6));
    int *i = p.get();
    *i = *i + 4;
    cout << *(p.get())  << endl;
}

int main()
{
    test();
}

Компилируем и запускаем:

 Tims-MacBook-Pro:cool_ptr tim$ clang++ cool_test.cpp -o cool_ptr
Tims-MacBook-Pro:cool_ptr tim$ ./cool_ptr 
Construct
10
Delete
Tims-MacBook-Pro:cool_ptr tim$ 

То есть, в функции test() мы сконструировали нашу обертку, положили в нее указатель на int. Указатель на int мы создали через new. Т.е. запросили память у операционной системы. Однако освобождать нам ее не надо. За нас это делает наша обертка. Как только мы выходим из функции test(), мы покидаем область видимости p. После чего, p удаляется и удаляет связанные с собой ресурсы.

Вот так. Мы поговорили немного про один из видов умных указателей. Зачем он нужен. Затронули исключения в конструкторе и частичное создание объектов. Написали свою реализацию умного указателя. И… вспомнили немножко шаблоны в С++, обобщенное программирование.

Оставьте комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *