cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
90 stars 0 forks source link

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

Open apolukhin opened 3 years ago

apolukhin commented 3 years ago

Перенос предложения: голоса +5, -1 Автор идеи: ??

Для обеспечения нужного уровня безопасности исключений бывает необходимо при выходе из области видимости выполнить какой-то код (например, освободить ресурс) даже, если код прерван исключением. При этом не всегда удобно создавать отдельный объект, управляющий ресурсом, например, если идет работа с чисто 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 давно пора стандартизировать.

apolukhin commented 3 years ago

Игорь, 29 ноября 2017, 14:01 То есть, если мы не выполним .release() у нас по ошибке может произойти двойное очищение ресурсов? Как то не безопасно, следить получается нужно будет за тем чтобы не забыть написать .release() и чтобы выполнение кода дошло до .release() и чтобы release() находился после ручного освобождения ресурсов.

Если такое и делать, то тогда release внутри должен не просто переключать флаг, а еще и выполнять код. Тогда, вероятно это будет работать. Но при этом нужно будет помнить, что вне функции .release освобождать ресурс нельзя. И тогда, можно спокойно забывать писать функцию release и guard вызовет code при своем уничтожении

Antervis, 1 декабря 2017, 22:41 с таким синтаксисом можно и unique_ptr для подобного использовать. Хорошая реализация бы выглядела как-то так: scoped_exit(=) { close(fd); } Стоит ли тащить в ядро языка то, что можно сделать на макросе, при тенденции уходить от макросов?

Andrey Davydov, 4 декабря 2017, 11:50 Proposal на подобную штуку (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0052r3.pdf) уверенно движется в C++20 (на сколько я могу судить со стороны, сам я никакого отношения к работе комитета не имею).

apolukhin commented 3 years ago

В C++20 не пролезло https://wg21.link/p0052 , решили внести в Library Funcdamentals TS 3