Несколько дней новогоднего волшебства:
Успейте начать обучение в 2018-ом году со скидкой до 30%!
Выбрать курс

ScopeGuard: одним велосипедом стало меньше

С++Deep2Site.png Все, кто хоть раз восхищался нововведениями стандарта C++11 (давно это было, но восхищаться можно бесконечно), знают о существовании интеллектуальных указателей, которые позволяют не беспокоиться о корректной очистке памяти. Также вряд ли был обделён вниманием новый класс std::lock_guard, который позволяет не беспокоиться об освобождении мьютекса:

void superFunc()
{
    // здесь мьютекс будет захвачен
    std::lock_guard<std::mutex> guard(someMutex);
    // тут сложная логика
} // при выходе из функции мьютекc будет освобождён

Как было бы здорово распространить эту идиому на другие ресурсы, которые требуют каких-либо иных действий по окончанию использования (не освобождение памяти, например, а закрытие файла). Такой код понятнее, проще писать и сопровождать. Стоит подумать некоторое время над решением.

Пишем сами

Проблем в общем-то нет никаких. Можно реализовать свой класс ScopeGuard с шаблонными параметрами значения и пользовательской delete-функцией:
template<typename T, typename F>
class ScopeGuard
{
public:
    template<typename TT, typename FF>
    ScopeGuard(TT&& value, FF&& deleter)
        :
        m_val(std::forward<TT>(value))
        , m_f(std::forward<FF>(deleter))
    {
    }

ScopeGuard(ScopeGuard &&that) : m_val(std::move(that.m_val)) , m_f(std::move(that.m_f)) , m_isSet(that.m_isSet) { that.m_isSet = false; }

~ScopeGuard() { destroy(); }

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

T& get() { return m_val; }

T release() { m_isSet = false; return m_val; }

template<typename TT> void reset(TT && value) { destroy(); m_val = std::forward<TT>(value); m_isSet = true; } private: void destroy() { if (m_isSet) try { m_f(m_val); } catch (...) {}; }

bool m_isSet{ true }; T m_val; F m_f; };

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

template<typename T, typename F>
ScopeGuard<std::remove_reference_t<T>, F> makeScopeGuard(T value, F&& deleter) {
    return ScopeGuard<T, F>(
	std::forward<T>(value), std::forward<F>(deleter)
    );
}

Использовать примерно так:

int main(int argc, char * argv[])
{
    {
        // Открываем файл и сразу помещаем его в guard
        auto fileGuard = makeScopeGuard(
		fopen("someFile", "r"), 
		[](FILE * hFile) { fclose(hFile); }
	 );
        // работаем с файлом
        fileGuard.get();

// на выходе из scope файл закрывается } return 0; }

Цель достигнута, можно гордиться ещё одним написанным классом общего назначения, который будут использовать наши потомки. Кладём его в utils и с гордостью коммитим в репозиторий.

Используем готовое

В предложенном выше решении всё хорошо: и работает, и красиво, и можно похвастаться перед коллегами, а может даже добавить в личное портфолио. Но что, если вдруг потребуется Guard не с передачей владения (move-семантикой), а с возможностью копирования и подсчётом ссылок? Что ж, напишем новый ScopeSharedGuard. А тот, что выше, переименуем в ScopeUniqueGuard. Кажется, что-то напоминает, да? Что если переписать наши make-функции вот так:
template<typename T, typename F>
auto makeUniqueGuard(T* value, F &&deleter)
{
    return std::unique_ptr<T, F>(value, std::forward<F>(deleter));
}

template<typename T, typename F> auto makeSharedGuard(T* value, F && deleter) { return std::shared_ptr<T>(value, std::forward<F>(deleter)); }

Использовать можно так же, как и в прошлый раз (только лучше, потому что теперь у нас автоматом поддерживаются обе семантики: как передача владения, так и подсчёта ссылок):
int main(int argc, char * argv[])
{
    {
        // Открываем файл и сразу помещаем его в unique-guard
        auto uniqueGuard = makeUniqueGuard(
		fopen("someFile", "r"), 
		[](FILE * hFile) { fclose(hFile); }
	 );
        // Открываем файл и сразу помещаем его в shared-guard
        auto sharedGuard = makeSharedGuard(
		fopen("someFile", "r"), 
		[](FILE * hFile) { fclose(hFile); }
	 );
        // работаем с файлом
        uniqueGuard.get();
        // и не забываем про второй
        sharedGuard.get();
    }// на выходе из scope оба файла закрывается

return 0; }

Из достоинств такого подхода:

1. Лишним велосипедом на свете стало меньше (вернее, не стало больше);
2. Получили обе целевые семантики – владение и подсчёт ссылок.

Из недостатков:

1. Работает только с указателями;
2. Больше нет поводов для гордости;
3. Маловато для размещения в портфолио.

Автор
7 комментариев
0

Спасибо, полезная статья, вот только форматирование кода надо поправить.

0

Поправил. Теперь, вроде, можно читать.

0

.

0

.

0

А в чем отличие от умного указателя с указанием deleter'а?

0

Хм.. В итоге как раз и пришли к использованию умного указателя. В конце статьи.

0

Одно из отличий - умный указатель "Работает только с указателями;" Если кроме закрытия указателя надо, например, написать в лог, то с умным указателем надо делать класс в деструкторе которого всё выполнять. С помощью ScopeGuard можно обойтись без создания дополнительных классов. У Andrei Alexandrescu есть видео про Declarative Code Flow. Там он даёт пример подобного ScopeGuard с дополнительной обёрткой.

Для комментирования необходимо авторизоваться