Об RAII замолвите слово

 

Давненько я сюда не писал ничего технического, настало время заполнить сий пробел. Считаю важным зафиксировать здесь несколько заметок о технологии RAII (Resource Acquisition Is Initialization, получение ресурса есть инициализация). Еще информацию об этой технологии на просторах англоязычного интернета можно найти по названию Scoped-Based Resource Management (SBRM), по русски это будет звучать как-то так – управление ресурсами на основе области видимости.

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

Давайте проиллюстрируем важность и удобство техники на примере.

Например, у нас есть функция, которая получает мьютекс в начале и освобождает его в конце:

void foo( Mutex& mutex)
{
    mutex.acquire( );

    // выполняем какой-нибудь код

    mutex.release( );
}

Позже, может понадобится добавить досрочный выход из функции по какому-нибудь условию. Пока функция небольшая по размерам, не проблема увидеть, что функция получается мьютекс и освобождает его в конце, поэтому необходимо добавить освобождение мьютекса при досрочном выходе:

void foo( Mutex& mutex )
{
    mutex.acquire( );

    // выполняем какой-нибудь код

    if ( shoudExit )
    {
        // неплохо бы сделать следующее
        mutex.release( );
        return;
    }

    // выполняем какой-нибудь код

    mutex.release( );
}

По прошествии длительного времени (год, например) функция разрастается и начинает занимать много строк кода. В процессе роста было добавлено много досрочных выходов, но освобождение мьютекса всегда добавлялось. Однажды, над кодом начинает работать новый человек, он решает добавить очередной досрочный выход… Из-за больших размеров функции мьютекса он может и не увидеть:

void foo( Mutex& mutex )
{
    mutex.acquire( );

    // очень много строк кода

    if ( newShouldExit )
    {
        // опаньки ...
        return;
    }

    // очень много строк кода

    mutex.release( );
}

Из примера видно, что такая техника управления ресурсами очень чувствительна к человеческой ошибке. Поэтому в таких случаях целесообразно применять RAII. Согласно стандарту С++ если мы определяем объект на стеке, его конструктор всегда будет выполнен в процессе инициализации, а деструктор в момент выхода из зоны видимости, то есть при return. Поэтому давайте использовать это соглашение в своих целях для управления ресурсами. Для начала напишем вспомогательный класс для автоматического получения и освобождения мьютекса:

class MutexLock
{
public:

    MutexLock( Mutex& mutex )
        :    _mutex( mutex )
    {
        _mutex.acquire( );
    }

    ~MutexLock( )
    {
        _mutex.release( );
    }

private:

    Mutex& _mutex;
}

Все что требуется – это получить мьютекс в начале нашей функции, и нет больше нужды беспокоиться об его освобождении перед каждым возвратом из функции, потому что об освобождении позаботится деструктор:

void foo( Mutex& mutex )
{
    MutexLock lock( mutex );

    // очень много строк кода

    if ( newShouldExit )
    {
        // теперь нет необходимости освобождать мьютекс здесь
        return;
    }

    // очень много строк кода

    if ( shouldExit )
    {
        // и здесь тоже не надо
        return;
    }

    // очень много строка кода

    // и в конце также не надо
}

Эту технику можно использовать в разных сценариях. Например, если в начале функции выделяется память из кучи, и требуется, чтобы память освобождалась в момент выхода из функции, можно использовать умные указатели STL. Ниже приведен скелет реализации умного указателя с использованием обозначенной техники:

template < typename T >
class SmartPointer
{
public:

    SmartPointer( T* ptr )
        :    _ptr( ptr )
    {
    }

    ~SmartPointer( )
    {
        delete _ptr;
    }

    T& operator * ( )
    {
        return *_ptr;
    }

    T* operator -> ( )
    {
        return _ptr;
    }

private:

    T* _ptr;
}

Наша функция в свою очередь будет выглядеть так:

void foo( )
{
    // выделяем память
    SmartPointer ptr( new MyClass( ) );

    // очень много строк кода

    // это работает правильно, т.к. мы переопределили оператор ->
    ptr->DoSomething( );

    if ( shouldExit )
    {
        // память автоматически очищается в деструкторе
        return;
    }

    // очень много строк кода

    // память автоматически очищается здесь тоже
}

С памятью есть еще один интересный пример. Иногда требуется неким образом занять всю доступную память при запуске. Далее мы будем использовать эту память по своему усмотрению с помощью технологии placement new. Соответственно куски такой «сырой» превыделенной памяти мы назовем контекстами памяти. Соответственно мы имеем глобальный набор таких контекстов, после чего мы решаем, из какого контекста будет возвращен указатель на «выделенную» память.

Если дальше необходимо использовать какой-то контекст в рамках области видимости функции, то опять наблюдается уже знакомый паттерн:

class MemoryChunkUse
{
public:

    MemoryChunkUse( MemoryChunk& chunk )
        :    _chunk( chunk )
    {
        UseMemoryChunk( chunk );
    }

    ~MemoryChunkUse( )
    {
        ReleaseMemoryChunk( chunk );
    }

private:

    MemoryChunk& _chunk;
}

Функция будет выглядеть так:

void foo( )
{
    // используем контекст
    MemoryChunkUse useChunk( GRAPHICS_MEMORY_CHUNK );

    // очень много строк кода

    if ( shouldExit )
    {
        // здесь контекст памяти будет автоматически освобожден
        return;
    }

    // очень много строк кода

    // здесь контекст памяти также будет освобожден
}

С помощью технологии RAII вы заставляете компилятор освобождать ресурсы за вас, причем практически бесплатно. Круто, не так ли?