Смарт-поинтеры и RAII на службе программиста

в 10:04, , рубрики: c++, c++11, raii, метки: ,

Исторически так сложилось, что руководство желает, чтобы задача была выполнена быстро. Для этого программисты сохраняют красоту и чистоту кода. Этот пост появился как напоминание о редкоиспользуемых нововведениях в C++11 – смарт-поинтерах, позволяющих указывать функтор для освобождения ресурсов.
Для примера возьмем файловый поток FILE из stdio.h, который любят за простоту и скорость, попробуем добавить ему красоту и базовую гарантию при исключениях:

unique_ptr<FILE, decltype(&fclose)> my_file(fopen("test.txt", "w"), &fclose);
if(my_file)
  fwrite("test", 4, 1, my_file.get());

В результате код зависит только STL и требует небольшой модификации обращений к файлу, пишется быстро, выглядит современно. Вот так получился RAII в чистом виде.

Как это работает?

Функция fopen возвращает указатель на объект типа FILE, который сохраняется в переменной my_file вместе с указателем на функцию fclose. Таким способом владение данным файловым потоком передается локальной переменной.

Когда функция fclose будет вызвана автоматически?

  1. При выходе из области видимости переменной (например, из функции).
  2. При возникновении исключения после создания my_file.
  3. При вызове функции присваивания объекту my_file.
  4. При вызове my_file.reset().

Какие накладные расходы?

  1. Программисту требуется усложнить создание файла, удалить вызов fclose и дополнить вызовом unique_ptr<…>::get() все обращения к файлу.
  2. Компилятору в худшем случае потребуется ячейка памяти для хранения указателя на функцию удаления файла. В лучшем случае, он просто поставит вызов fclose в нужном месте за вас, полностью оптимизировав объект my_file.

Какие плюсы у данного подхода?

  1. Как и с любым смарт-поинтером, вы явно указываете способ владения объектом. В данном случае, указано, что объект не является общим (unique_ptr).
  2. Можно избавиться от лишнего copy-paste объявив свой тип так:
    typedef unique_ptr<FILE, decltype(&fclose)> MyFileType;
  3. Если используется много файлов, есть смысл написать небольшую обертку
    MyFileType MakeFile(const char* filename, const char* mode)
    {
      return unique_ptr<FILE, decltype(&fclose)>(fopen(filename, mode), &fclose);
    }
    

    … и пользоваться ей так:

    auto my_file = MakeFile("test.txt", "w");
  4. Позволяет избавиться от написания лишнего кода в деструкторе. Почему лишнего? Вы уже указали компилятору, как вы хотите управлять этим ресурсом и теперь это его работа.
  5. Можно использовать объекты типа MyFileType в стандартных контейнерах STL:
    vector<MyFileType> my_files;
    my_files.push_back(MakeFile("test.txt", "w"));
    

    … и не тратить своё время на контроль времени жизни объектов. В C++11 vector<MyFileType> можно смело возвращать из функции.

Вот еще несколько идей из C Runtime Library:

Те, кто озадачен или увлекается оптимизацией под Windows знает, что доступ к выровненным данным происходит быстрее. Так можно создать указатель на память, выровненную на 16 байт используя библиотеку Microsoft Visual C Runtime:

unique_ptr<char[], decltype(&::_aligned_free)> my_buffer((char*)(_aligned_malloc(512, 16)), &_aligned_free);
my_buffer[0] = ‘x’; //  использование буфера

Написав один раз шаблон:

template<typename T>
unique_ptr<T[], decltype(&::_aligned_free)>
MakeAlignedBuffer(size_t element_count, size_t alignment = alignment_of<T>::value)
{
	return unique_ptr<T[], decltype(&::_aligned_free)>
		(reinterpret_cast<T*>(_aligned_malloc(element_count*sizeof(T), alignment)), &_aligned_free);
}

можно забыть об ошибках выделения и удаления памяти разными функциями (создали через new[] в одном модуле, удалили через delete в другом).

А что делать, если определенным WinAPI ресурсом владеет несколько объектов?

Для примера рассмотрим ситуацию, когда в GUI приложении несколько разных объектов используют функции, которые находятся в динамически-загружаемой DLL. В таком случае, не так легко запрограммировать своевременную выгрузку библиотеки как хотелось бы:
Загружаем библиотеку…

auto my_module = shared_ptr<HMODULE>(new HMODULE(LoadLibrary(_T("my_library.dll"))), [](HMODULE* instance){
	FreeLibrary(*instance);  //  выгружаем библиотеку когда ссылок на нее больше нет
});

Далее раздаем my_module объектам…

module_owner1.set_module(my_module);
module_owner2.set_module(my_module);  //  или можем хоть в vector их сложить

В объекте используем нужные функции…

if(my_module && *my_module)
{
	auto func1 = GetProcAddress(*my_module, "MyFunc");
}

Когда функциями перестаем пользоваться и счетчик ссылок на объект станет равен нулю – объект my_module будет вызвана функция FreeLibrary и объект будет удален.

Как использовать лямбда-функцию в unique_ptr?

Необходимо воспользоваться шаблоном function вот так:

auto my_instance = std::unique_ptr<HMODULE, function<void(HMODULE*)>>
                    (new HMODULE(LoadLibrary(_T("my_library.dll"))), [](HMODULE* instance){ FreeLibrary(*instance); });

Заключение

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

Автор: wizardsd

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js