Подтвердить что ты не робот

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

Инициализация ресурсов - это Инициализация (RAII) обычно используется в С++ для управления временем жизни ресурсов, которые требуют некоторого способа очистки кода в конце их жизни, от delete ing new ed указатели на выпуск дескрипторов файлов.

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

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

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

4b9b3361

Ответ 1

Существует простой способ использования RAII для управления ресурсами из интерфейса C-стиля: стандартные интеллектуальные указатели библиотеки, которые представлены в двух вариантах: std::unique_ptr для ресурсов с одним владельцем и командой std::shared_ptr и std::weak_ptr для общих ресурсов. Если у вас возникли проблемы с выбором вашего ресурса, этот Q & A должен помочь вам решить. Доступ к необработанному указателю, который управляет интеллектуальный указатель, так же просто, как вызов его функции 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 Не верьте мне? вы можете видеть его в действии на Coliru, где я предоставил некоторые фиктивные реализации для функций получения и выпуска, чтобы вы могли увидеть, как это происходит.

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

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

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

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

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

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

  • вызов acquireResource
  • Конструкция ThrowsOnConstruction
  • построить std::unique_ptr из указателя ресурсов

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

Опять же, как упоминалось в GotW # 56, на самом деле существует относительно простой способ справиться с проблемой безопасности исключений. В отличие от оценок выражений в аргументах функций, оценки функций не могут чередоваться. Поэтому, если мы приобретем ресурс и передадим его unique_ptr внутри функции, мы гарантируем, что не будет сложного бизнеса, чтобы утечка нашего ресурса, когда ThrowsOnConstruction бросает на конструкцию. Мы не можем использовать std::make_unique, потому что он возвращает std::unique_ptr с дефолтом по умолчанию, и мы хотим, чтобы наш собственный пользовательский аромат deleter. Мы также хотим указать нашу функцию сбора ресурсов, поскольку она не может быть выведена из типа без дополнительного кода. Реализация такой вещи достаточно проста с мощью шаблонов: 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};
}

Live on Coliru

вы можете использовать его следующим образом:

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{});

Edit:

Как уже упоминалось в комментариях, можно сделать еще один шаг к использованию std::unique_ptr: указать механизм удаления во время компиляции, чтобы unique_ptr не нужно было переносить указатель на объект, если он перемещался по программе. Для создания шаблона, не использующего stateless, для используемого указателя функции требуется четыре строки кода, помещенные перед 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);

Live on Coliru

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


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

Ответ 2

Специальный механизм защиты области может чисто и кратко управлять ресурсами стиля С. Поскольку это относительно старая концепция, существует число, плавающее вокруг, но возможности защиты, которые позволяют произвольное выполнение кода, по своей природе являются наиболее гибкими из них. Два из популярных библиотек: SCOPE_EXIT, из библиотеки с открытым исходным кодом из facebook folly (обсуждалось в Andrei Alexandrescu talk on Declarative Control Flow) и BOOST_SCOPE_EXIT из (неудивительно) Boost.ScopeExit.

folly 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 после успешного выполнения соответствующих операций.