Смарт-поинтеры и 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); });

Заключение


Уважаемые читатели, помните, что любая технология разрабатывается с определенной целью и не должна использоваться там, где её использование не оправдано, т.е. не стоит бросаться заменять все указатели на смарт-поинтеры не задумываясь о необходимости и не анализируя последствия. Минусы у этих подходов тоже есть и они неоднократно обсуждались на хабре. Будьте профессиональны.
Спасибо.
  • +14
  • 9.3k
  • 8
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 8

    0
    редкоиспользуемых нововведениях в C++11 – смарт-поинтерах

    Ну не сказал бы.

    Да, вроде как std::vector<MyFileType> может быть использовано только в С++11, т.к. у std::unique_ptr есть только конструктор перемещения, т.е. в вектор его можно положить, если у того тоже он есть.
      0
      С указанием функции освобождения ресурса используется не так часто, как без. Да, unique_ptr является частью стандартной библиотеки шаблонов C++11, где и все контейнеры умеют работать с типами, которые можно только перемещать (но не копировать).
      +4
      Спасибо, за статью!

      Если производительность критична, то в последнем примере я бы не стал заворачивать лямбду в function. Зачем лишние накладные расходы на упаковку и полиморфный вызов?

      auto lambda = [](HMODULE* instance){ FreeLibrary(*instance); };
      auto my_instance = std::unique_ptr<HMODULE, decltype(lambda)>
          (new HMODULE(LoadLibrary(_T("my_library.dll"))), lambda);
      
      FreeLibrary(*instance) теперь просто заинлайнится.

      Здесь еще более серьезная проблема в том, что Вы выделяете через new HMODULE память под хендл в куче и не освобождаете ее, Вы ведь заместили deleter по-умолчанию своим, не так ли? Исправляем вот так:
      auto lambda = [](HMODULE* instance){ FreeLibrary(*instance); delete instance;};
      


      В итоге, я бы извратился еще проще:

      auto lambda = [](HINSTANCE instance){ FreeLibrary(instance); };
      auto my_instance = std::unique_ptr<HINSTANCE__, decltype(lambda)>
          (LoadLibrary(_T("my_library.dll")), lambda);
      

      Никаких new, delete и куч. Для shared_ptr примера то же самое.

      С Наступающим, Вас и всех!
        0
        Осталось дождаться (или написать самому) std::make_unique, чтобы переложить написание всех этих decltype на ADL.
          0
          К сожалению, у Microsoft реализации STL обычно запаздывают за компилятором (по понятным причинам), но ADL то есть!
          Поэтому для тех, кто не ждет:
          1. Подойдет для всего, что умеет unique_ptr и forward: pastebin.com/3vU6CAnN
          2. Более простой вариант для VS2012: stackoverflow.com/a/13883981/1190077

          Не хотелось загромождать этим пост, хотя не спорю, что так безопаснее.
            0
            make_unique уже в VS 2013.
          0
          В примере с fopen/fclose плохо то, что fclose будет вызван даже в случае, если fopen был неуспешен (возвратил null pointer). Насколько безопасно вызывать fclose для null pointer (стандарт как бы говорит нам, что behavior undefined)? Поэтому вариант с лямбдой, в которой добавлена проверка, что освобождаемый ресурс был инициализирован, кажется более подходящим.

          Да и вообще, RAII-шаблон для произвольного типа у которого есть deleter(T) и T(invalid_value) пишется легко и непринуждённо (с поддержкой move семантики, ежели захочется) — примеров в интернетах море. Мне кажется он будет несколько проще, чем обёрнутый/допиленный unique_ptr.
            +2
            Нет, этот вариант вполне уместен. Согласно стандарта С++11 в моём примере функция fclose будет вызвана только тогда, когда fopen вернет что-то отличное от nullptr.
            20.7.1.2.2 unique_ptr destructor [unique.ptr.single.dtor]
            1. Requires: The expression get_deleter()(get()) shall be well formed, shall have well-defined behavior,
            and shall not throw exceptions.
            2. Effects: If get() == nullptr there are no effects. Otherwise get_deleter()(get()).

          Only users with full accounts can post comments. Log in, please.