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

Курсы

Курсы в разработке Подготовительные курсы
Работа в компаниях Компаниям Блог +7 499 110-61-65

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

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

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

Пишем сами

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

ScopeGuard(ScopeGuard &amp;&amp;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&amp;) = delete; ScopeGuard&amp; operator=(const ScopeGuard&amp;) = delete; ScopeGuard&amp; operator=(ScopeGuard&amp;&amp;) = delete;

T&amp; get() { return m_val; }

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

template&lt;typename TT&gt; void reset(TT &amp;&amp; value) { destroy(); m_val = std::forward&lt;TT&gt;(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&lt;typename T, typename F&gt;
ScopeGuard&lt;std::remove_reference_t&lt;T&gt;, F&gt; makeScopeGuard(T value, F&amp;&amp; deleter) {
    return ScopeGuard&lt;T, F&gt;(
	std::forward&lt;T&gt;(value), std::forward&lt;F&gt;(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&lt;typename T, typename F&gt;
auto makeUniqueGuard(T* value, F &amp;&amp;deleter)
{
    return std::unique_ptr&lt;T, F&gt;(value, std::forward&lt;F&gt;(deleter));
}

template&lt;typename T, typename F&gt; auto makeSharedGuard(T* value, F &amp;&amp; deleter) { return std::shared_ptr&lt;T&gt;(value, std::forward&lt;F&gt;(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. Маловато для размещения в портфолио.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

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

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

0

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

0

.

0

.

0

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

0

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

0

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

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