scope_guard и on_exception_guard для освобождения ресурсов

masterspline
masterspline

Для обеспечения нужного уровня безопасности исключений бывает необходимо при выходе из области видимости выполнить какой-то код (например, освободить ресурс) даже, если код прерван исключением. При этом не всегда удобно создавать отдельный объект, управляющий ресурсом, например, если идет работа с чисто C-шным кодом. Для этого я предлагаю стандартизировать scope_guard и on_exception_guard, со вспомогательными методами их создания make_scope_guard() make_on_exception_guard().

int main()
{
    int fd = ::open( "/etc/passwd", O_RDWR );
    if( fd == -1 )
        exit( EXIT_FAILURE );
    auto fd_guard = make_scope_guard( [=](){ ::close( fd ); } );
    // make_scope_guard() создает объект scope_guard, в деструкторе которого выполнится переданный код
    do_somth_with( fd );
    close( fd );        // если закрывать fd до выхода из области видимости не нужно,
    fd_guard.release(); // тогда эти две строки можно вообще не писать

    do_some_more();
    
    return 0;
}

При использовании такого кода в реальности оптимизитор выбросит все ненужное, в т.ч. и создание объекта scope_guard, при этом сгенерирует код, в котором при любом выходе из области видимости будет вызван close().

При вызове make_scope_guard(), создается объект scope_guard и ему передается лямбда для выполнения в деструкторе ~scope_guard(). Также у scope_guard есть метод release() для отмены выполнения кода в деструкторе и для симметричности acquire(), хотя не уверен, что он нужен. Примерная реализация:

template <typename T>
struct scope_guard
{
    scope_guard( T code )
        :code( code )
        ,own( true )
    {}
    scope_guard( scope_guard&& other )
        :code( other.code )
        ,own( true )
    { other.own = false; }
    scope_guard& operator=( scope_guard&& other ) = delete;
    ~scope_guard() { if( own ) code(); }

    scope_guard( const scope_guard& ) = delete;
    scope_guard& operator=( const scope_guard& ) = delete;

    void release() { own = false; }
    void acquire() { own = true; }
    T code;
    bool own;
};

/**
 * @code{.cpp}
 * auto fd_guard = make_scope_guard( [=](){ ::close( fd ); } );
 * @endcode
 */
template <typename T>
scope_guard<T> make_scope_guard( T code )
{
    return scope_guard<T>( code );
}

У такого подхода есть один мелкий недочет - при создании scope_guard может вылететь исключение, однако, это возможно только при копировании захватываемых переменных по значению, однако в реальности все сколько-нибудь нетривиальные переменные scope_guard будет захватывать по ссылке.

on_exception_guard отличается от scope_guard лишь тем, что выполняет переданный код в деструкторе только при выходе из области видимости по исключению. В нужности методов release(), а тем более acquire() я сильно сомневаюсь. Реализация:

template <typename T>
struct on_exception_guard
{
    on_exception_guard( T code )
        :code( code )
    {}
    ~on_exception_guard()
    {
#ifdef __cpp_lib_uncaught_exceptions
        if ( std::uncaught_exceptions() > 0 )
#else
        if ( std::uncaught_exception() )
#endif
            code();
    }
    T code;
};

template <typename T>
on_exception_guard<T> make_on_exception_guard( T code )
{
    return on_exception_guard<T>( code );
}

P.S. Можно обсуждать имена методов и типов, их функциональность и реализацию, но, по-моему, бесспорно, что нечто с функциональностью scope_exit давно пора стандартизировать.

1
рейтинг
3 комментария
rumyancev95
То есть, если мы не выполним .release() у нас по ошибке может произойти двойное очищение ресурсов? Как то не безопасно, следить получается нужно будет за тем чтобы не забыть написать .release() и чтобы выполнение кода дошло до .release() и чтобы release() находился после ручного освобождения ресурсов.

Если такое и делать, то тогда release внутри должен не просто переключать флаг, а еще и выполнять код. Тогда, вероятно это будет работать. Но при этом нужно будет помнить, что вне функции .release освобождать ресурс нельзя. И тогда, можно спокойно забывать писать функцию release и guard вызовет code при своем уничтожении
rumyancev95
Antervis
с таким синтаксисом можно и unique_ptr для подобного использовать. Хорошая реализация бы выглядела как-то так: scoped_exit(=) { close(fd); } Стоит ли тащить в ядро языка то, что можно сделать на макросе, при тенденции уходить от макросов?
Antervis
Andrey Davydov
Proposal на подобную штуку (open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0052r3.pdf) уверенно движется в C++20 (на сколько я могу судить со стороны, сам я никакого отношения к работе комитета не имею).
Andrey Davydov
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).