Исторически так сложилось, что руководство желает, чтобы задача была выполнена быстро. Для этого программисты сохраняют красоту и чистоту кода. Этот пост появился как напоминание о редкоиспользуемых нововведениях в 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 будет вызвана автоматически?
- При выходе из области видимости переменной (например, из функции).
- При возникновении исключения после создания my_file.
- При вызове функции присваивания объекту my_file.
- При вызове my_file.reset().
Какие накладные расходы?
- Программисту требуется усложнить создание файла, удалить вызов fclose и дополнить вызовом unique_ptr<…>::get() все обращения к файлу.
- Компилятору в худшем случае потребуется ячейка памяти для хранения указателя на функцию удаления файла. В лучшем случае, он просто поставит вызов fclose в нужном месте за вас, полностью оптимизировав объект my_file.
Какие плюсы у данного подхода?
- Как и с любым смарт-поинтером, вы явно указываете способ владения объектом. В данном случае, указано, что объект не является общим (unique_ptr).
- Можно избавиться от лишнего copy-paste объявив свой тип так:
typedef unique_ptr<FILE, decltype(&fclose)> MyFileType;
- Если используется много файлов, есть смысл написать небольшую обертку
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");
- Позволяет избавиться от написания лишнего кода в деструкторе. Почему лишнего? Вы уже указали компилятору, как вы хотите управлять этим ресурсом и теперь это его работа.
- Можно использовать объекты типа 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