Использование RAII для управления ресурсами из API в стиле C

Приобретение ресурса — Инициализация (RAII) обычно используется в C ++ для управления временами жизни ресурсов, которые требуют некоторого способа очистки кода в конце их жизненного цикла, от deleteИНГ newУказатели на выпуск файловых дескрипторов.

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

В моем случае я хочу использовать RAII для автоматического выполнения функции очистки из API в стиле C, когда переменная, содержащая высвобождаемый им ресурс в стиле C, выходит из области видимости. Мне действительно не нужно дополнительного переноса ресурсов, и я бы хотел минимизировать накладные расходы кода при использовании RAII здесь. Есть ли простой способ использовать RAII для управления ресурсами из API в стиле C?

Как инкапсулировать C api в классы RAII C ++? связано, но я не верю, что это дубликат — этот вопрос касается более полной инкапсуляции, в то время как этот вопрос касается минимального кода для получения преимуществ RAII.

18

Решение

Существует простой способ использовать RAII для управления ресурсами из интерфейса в стиле C: стандартная библиотека умные указатели, которые бывают двух видов: std::unique_ptr за ресурсы с одним владельцем и командой std::shared_ptr а также std::weak_ptr для общих ресурсов. Если у вас возникли проблемы с выбором вашего ресурса, этот вопрос&А должен помочь вам решить. Доступ к необработанному указателю, которым управляет умный указатель, так же прост, как и вызов его get функция-член.

Если вы хотите простое, основанное на области управление ресурсами, std::unique_ptr это отличный инструмент для работы. Он спроектирован с минимальными затратами и прост в настройке с использованием пользовательской логики уничтожения. На самом деле это так просто, что вы можете сделать это, объявив переменную ресурса:

#include <memory> // allow use of smart pointers

struct CStyleResource; // c-style resource

// resource lifetime management functions
CStyleResource* acquireResource(const char *, char*, int);
void releaseResource(CStyleResource* resource);// my code:
std::unique_ptr<CStyleResource, decltype(&releaseResource)>
resource{acquireResource("name", nullptr, 0), releaseResource};

acquireResource выполняется там, где вы его вызываете, в начале срока жизни переменной. releaseResource будет выполняться в конце времени жизни переменной, обычно, когда она выходит из области видимости.1 Не веришь мне? вы можете увидеть это в действии на Колиру, где я предоставил несколько фиктивных реализаций для функций получения и выпуска, чтобы вы могли видеть, как это происходит.

Вы можете сделать то же самое с std::shared_ptr, если вам требуется этот ресурс времени жизни ресурса:

// my code:
std::shared_ptr<CStyleResource>
resource{acquireResource("name", nullptr, 0), releaseResource};

Теперь и то и другое хорошо, но стандартная библиотека имеет std::make_unique2 а также std::make_shared и одной из причин является дальнейшее исключение безопасности.

Получил # 56 упоминаний что оценка аргументов функции неупорядочена, что означает, что если у вас есть функция, которая принимает ваш блестящий новый std::unique_ptr type и некоторый ресурс, который может создать конструкцию, предоставляя этот ресурс для вызова функции, например:

func(
std::unique_ptr<CStyleResource, decltype(&releaseResource)>{
acquireResource("name", nullptr, 0),
releaseResource},
ThrowsOnConstruction{});

означает, что инструкции могут быть упорядочены следующим образом:

  1. вызов acquireResource
  2. сооружать ThrowsOnConstruction
  3. сооружать std::unique_ptr из указателя ресурса

и что наш драгоценный ресурс интерфейса C не будет очищен должным образом, если выберет шаг 2.

Опять же, как упоминалось в GotW # 56, на самом деле существует относительно простой способ решения проблемы безопасности исключений. В отличие от вычислений выражений в аргументах функций, функция оценки не могут быть чередованы. Так что, если мы приобретаем ресурс и отдаем его unique_ptr внутри функции, мы будем гарантированы, что никакой хитрый бизнес не случится, если утечка нашего ресурса ThrowsOnConstruction броски на стройку. Мы не можем использовать std::make_uniqueпотому что он возвращает std::unique_ptr с удалением по умолчанию, и мы хотим, чтобы наш собственный вариант удаления. Мы также хотим указать нашу функцию получения ресурсов, так как она не может быть выведена из типа без дополнительного кода. Реализация такой вещи достаточно проста с помощью шаблонов:3

#include <memory> // smart pointers
#include <utility> // std::forward

template <
typename T,
typename Deletion,
typename Acquisition,
typename...Args>
std::unique_ptr<T, Deletion> make_c_handler(
Acquisition acquisition,
Deletion deletion,
Args&&...args){
return {acquisition(std::forward<Args>(args)...), deletion};
}

Жить на Колиру

Вы можете использовать это так:

auto resource = make_c_handler<CStyleResource>(
acquireResource, releaseResource, "name", nullptr, 0);

и позвонить func без проблем, вот так:

func(
make_c_handler<CStyleResource(
acquireResource, releaseResource, "name", nullptr, 0),
ThrowsOnConstruction{});

Компилятор не может взять конструкцию ThrowsOnConstruction и вставь его между звонками acquireResource и строительство unique_ptrтак что ты в порядке.

shared_ptr эквивалентно так же просто: просто поменяйте std::unique_ptr<T, Deletion> вернуть значение с std::shared_ptr<T>и измените имя, чтобы указать общий ресурс:4

template <
typename T,
typename Deletion,
typename Acquisition,
typename...Args>
std::shared_ptr<T> make_c_shared_handler(
Acquisition acquisition,
Deletion deletion,
Args&&...args){
return {acquisition(std::forward<Args>(args)...), deletion};
}

Использование еще раз похоже на unique_ptr версия:

auto resource = make_c_shared_handler<CStyleResource>(
acquireResource, releaseResource, "name", nullptr, 0);

а также

func(
make_c_shared_handler<CStyleResource(
acquireResource, releaseResource, "name", nullptr, 0),
ThrowsOnConstruction{});

Редактировать:

Как уже упоминалось в комментариях, вы можете сделать еще одно усовершенствование в использовании std::unique_ptr: указание механизма удаления во время компиляции, чтобы unique_ptr не нужно нести указатель функции на средство удаления, когда оно перемещается по программе. Для создания удаляемого без сохранения состояния шаблона для указателя функции, который вы используете, требуется четыре строки кода, помещенные перед make_c_handler:

template <typename T, void (*Func)(T*)>
struct CDeleter{
void operator()(T* t){Func(t);}
};

Тогда вы можете изменить make_c_handler вот так:

template <
typename T,
void (*Deleter)(T*),
typename Acquisition,
typename...Args>
std::unique_ptr<T, CDeleter<T, Deleter>> make_c_handler(
Acquisition acquisition,
Args&&...args){
return {acquisition(std::forward<Args>(args)...), {}};
}

Синтаксис использования затем немного меняется, чтобы

auto resource = make_c_handler<CStyleResource, releaseResource>(
acquireResource, "name", nullptr, 0);

Жить на Колиру

make_c_shared_handler не выиграет от перехода к шаблонному удалителю, так как shared_ptr не содержит информацию об удалении, доступную во время компиляции.


1. Если значение умного указателя nullptr когда оно разрушено, оно не будет вызывать ассоциированную функцию, что очень удобно для библиотек, которые обрабатывают вызовы освобождения ресурсов с нулевыми указателями в качестве условий ошибки, например SDL.
2. std::make_unique был включен только в библиотеку в C ++ 14, поэтому, если вы используете C ++ 11, вы можете захотеть реализовать свой собственный—это очень полезно, даже если это не совсем то, что вы хотите здесь.
3. Это (и std::make_unique реализация связана в 2) зависит от вариационные шаблоны. Если вы используете VS2012 или VS2010, которые имеют ограниченную поддержку C ++ 11, у вас нет доступа к шаблонам с переменными параметрами. Реализация std::make_shared в этих версиях вместо этого было сделано с отдельными перегрузками для каждого номера аргумента и комбинации специализации. Сделайте из этого что хочешь.
4. std::make_shared на самом деле имеет более сложная техника, чем эта, но для этого нужно знать, насколько большим будет объект этого типа. У нас нет такой гарантии, так как мы работаем с интерфейсом в стиле C и можем иметь только предварительное объявление о типе нашего ресурса, поэтому здесь мы не будем беспокоиться об этом.

25

Другие решения

Выделенный механизм защиты области действия может четко и кратко управлять ресурсами в стиле C. Поскольку это относительно старая концепция, вокруг нее есть число, но защитные ограждения, которые позволяют выполнять произвольный код, по своей природе являются наиболее гибкими. Два из популярных библиотек SCOPE_EXIT, из библиотеки с открытым исходным кодом Facebook folly (обсуждается в беседе Андрея Александреску о декларативном контроле), а также BOOST_SCOPE_EXIT от (неудивительно) Boost.ScopeExit.

глупость-х SCOPE_EXIT является частью триады функциональности декларативного потока управления, представленной в <folly/ScopeGuard.hpp>. SCOPE_EXIT SCOPE_FAIL а также SCOPE_SUCCESS соответственно выполнять код, когда поток управления выходит из охватывающей области, когда он выходит из охватывающей области, выбрасывая исключение, и когда он выходит без выброса исключения.1

Если у вас есть интерфейс в стиле C с функциями управления ресурсами и временем жизни, такими как:

struct CStyleResource; // c-style resource

// resource lifetime management functions
CStyleResource* acquireResource(const char *, char*, int);
void releaseResource(CStyleResource* resource);

вы можете использовать SCOPE_EXIT вот так:

#include <folly/ScopeGuard.hpp>

// my code:
auto resource = acquireResource(const char *, char *, int);
SCOPE_EXIT{releaseResource(resource);}

Boost.ScopeExit имеет слегка различающийся синтаксис.2 Чтобы сделать так же, как приведенный выше код:

#include <boost/scope_exit.hpp>

// my code
auto resource = acquireResource(const char *, char *, int);
BOOST_SCOPE_EXIT(&resource) { // capture resource by reference
releaseResource(resource);
} BOOST_SCOPE_EXIT_END

В обоих случаях может оказаться целесообразным объявить resource как constчтобы избежать непреднамеренного изменения значения в течение оставшейся части функции и усложнять проблемы управления жизненным циклом, которые вы пытаетесь упростить.

В обоих случаях, releaseResource будет вызываться, когда поток управления выходит из окружающей области, по исключению или нет. Обратите внимание, что это будет также быть вызванным независимо от того, resource является nullptr в конце области, поэтому, если API требует, чтобы функции очистки не вызывались для нулевых указателей, вам необходимо проверить это условие самостоятельно.

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


1. Выполнение кода только в случае успеха или неудачи предлагает функциональность фиксации / отката, которая может быть невероятно полезной для обеспечения безопасности исключений и ясности кода, когда в одной функции может возникать несколько точек отказа, что, по-видимому, является причиной появления SCOPE_SUCCESS а также SCOPE_FAIL, но вы здесь, потому что вы заинтересованы в безусловной очистке.
2. Как примечание, Boost.ScopeExit также не имеет встроенной функции успеха / неудачи, такой как глупость. В документации функциональность успеха / неудачи, подобная той, что обеспечивается защитой области глупости, вместо этого реализуется путем проверки флага успеха, который был захвачен ссылкой. Флаг установлен в false в начале области и установите в true как только соответствующие операции завершатся успешно.

4