В пятницу выдался свободный вечер, такой когда срочных дел нет, а несрочные делать лень и хочется чего-то для души. Для души я решил посмотреть какой-нибудь доклад CppCon 2015 которая прошла чуть больше месяца назад. Как правило на видео доклады вживую у меня никогда времени не хватает, но тут все так уж сложилось — прошел месяц, C++-17 уже на носу и конференция должна была быть интересной, но никто еще ничего о ней не писал, а тут еще и вечер свободный.В общем я быстренько ткнул мышкой в первый привлекший внимание заголовок: Andrei Alexandrescu “Declarative Control Flow" и приятно провел вечер. А потом решил поделиться с хабрасообществом вольным пересказом.
Давайте вспомним что такое обычный для C++ Explicit Flow Control, напишем транзакционнo стабильную функцию для копирования файла, стабильную в том смысле что она имеет только два исхода: либо завершается успешно, либо по каким-либо причинам неуспешно, но при этом не имеет побочных эффектов, (красивое выражение — successful failure). Задача выглядит тривиальной, тем более если использовать boost::filesystem:
void copy_file_tr(const path& from, const path& to) {
    path tmp=to+".deleteme";
    try {
        copy_file(from, tmp);
        rename(tmp, to);
    } catch(...) {
        ::remove(tmp.c_str());
        throw;
    }
}
Что бы ни случилось во время копирования, временный файл будет удален, что нам и требовалось. Однако, если присмотреться здесь всего три строчки значимого кода, все остальное — проверка успешности вызова функций через try/catch, то есть ручное управление исполнением. Структура программы здесь не отражает реальную логику задачи. Еще один неприятный момент — этот код сильно зависит от явно неописанных здесь свойств вызываемых функций, так функция rename() предполагается атомарной (транзакционно стабильной), а remove() не должна выбрасывать исключений ( почему здесь и используется ::remove() вместо boost::filesystem::remove() ).Давайте еще усугубим и напишем парную функцию move_file_tr:
void move_file_tr(const path& from, const path& to) {
    copy_file_tr(from, to);
    try {
        remove(from);
    } catch(...) {
        ::remove(to.c_str());
        throw;
    }
}
Мы видим здесь все те же проблемы, в таком крохотном кусочке кода нам пришлось добавить еще один try/catch блок. Более того, даже здесь уже можно заметить насколько плохо такой код масштабируется, каждый блок вводит свою область видимости, пересечение блоков невозможно и т.д. Если вас все это еще не убедило, стандарт рекомендует свести к минимуму ручное использование try/catch, ибо «verbose and non-trivial uses error-prone».Давайте заявим прямо и честно что непосредственное управление деталями исполнения нас больше не устраивает, мы хотим большего.
Декларативный стиль вместо этого обращает основное внимание на описании целей, при этом детальные инструкции по достижению сведены к необходимому минимуму, исполнение кода правильным образом происходит без непосредственного контроля за исполнением каждого шага. Это могло бы звучать как фантастика, однако такие языки — вокруг нас и мы их используем каждый день не задумываясь. Посмотрите — SQL, make, regex, все они декларативны по своей природе. Что мы можем использовать в C++ чтобы достичь такого эффекта?
RAII и деструкторы имеют декларативную природу поскольку вызываются неявно, а также близкая идиома ScopeGuard. Давайте посмотрим как устроен макрос SCOPE_EXIT с использованием ScopeGuard, это на самом деле довольно старый трюк, достаточно сказать что одноименный макрос присутствует в boost начиная с версии 1.38. И тем не менее, повторение мать учения:
namespace detail {
    enum class ScopeGuardOnExit {};

    template<typename<Fun> ScopeGuard<Fun> operator+
    (ScopeGuardOnExit, Fun&& fn) {
        return ScopeGuard<Fun>(std::forward<Fun>(fn));
    }
}

#define SCOPE_EXIT \
    auto ANONIMOUS_VARIABLE(SCOPE_EXIT_STATE) \
    = ::detail::ScopeGuardOnExit + (&)[] 
}
Фактически, это половинка определения лямбда-функции, тело надо добавить при вызове.
Тут все достаточно прямолинейно, создается анонимная переменная содержащая ScopeGuard, который содержит лямбда-функцию, определенную непосредственно за вызовом макроса и которая функция будет вызвана в деструкторе этой переменной, который рано или поздно но при выходе из области видимости будет вызван. (В легких кончился воздух, а то бы я еще пару придаточных добавил)
Для полноты картины, вот так выглядят вспомогательные макросы:
#define CONACTENATE_IMPL(s1,s2) s1##s2
#define CONCATENATE(s1,s2) CONCATENATE_IMPL(s1,s2)
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str,__COUNTER__)
С использованием такой конструкции привычный C++ код разом приобретает невиданные разом черты:
void fun() {
    char name[] = "/tmp/deleteme.XXXXXX";
    auto fd = mkstemp(name);
    SCOPE_EXIT { fclose(fd); unlink(name); };
    auto buf = malloc(1024*1024);
    SCOPE_EXIT { free(buf); };
    ...
}
Так вот, утверждается что для полноценного перехода к декларативному стилю нам достаточно определить еще два подобных макроса — SCOPE_FAIL и SCOPE_SUCCESS, с использованием этой тройки можно разделить логически значимый код и детальные управляющие инструкции. Для этого нам необходимо и достаточно знать, вызывается деструктор, нормально или в результате отмотки стека. И такая функция есть в C++ — bool uncaught_exception(), она возвращает true если была вызвана изнутри catch блока. Однако тут есть один неприятный нюанс — эта функция в текущей версии C++ поломана и не всегда возвращает правильное значение. Дело в том что она не различает, является ли вызов деструктора частью размотки стека или это обычный обьект на стеке созданный внутри catch блока, подробнее почитать об этом можно из перво��сточника. Как бы то ни было, В C++-17 эта функция будет официально обьявлена deprecated и вместо нее введена другая — int uncaught_exceptions() (найдите сами два отличия), которая возвращает число вложенных обработчиков из которых была вызвана. Мы можем теперь создать вспомогательный класс, который точно покажет, вызывать SCOPE_SUCCESS или SCOPE_FAIL:
class UncaughtExceptionCounter {
    int getUncaughtExceptionCount() noexcept;
    int exceptionCount_;
public:
    UncaughtExceptionCounter()
    : exceptionCount_(std::uncaught_exceptions()) {}
    
    bool newUncaughtException() noexcept {
        return std::uncaught_exceptions() > exceptionCount_;
    }
};
Забавно что этот класс сам тоже использует RAII чтобы захватить состояние в конструкторе.
Вот теперь можно нарисовать полноценный шаблон который будет вызываться в случае успеха или неуспеха:
template <typename FunctionType, bool executeOnException>
class ScopeGuardForNewException {
    FunctionType function_;
    UncaughtExceptionCounter ec_;

public:
    explicit ScopeGuardForNewException(const FunctionType& fn)
    : function_(fn) {}

    explicit ScopeGuardForNewException(FunctionType&& fn)
    : function_(std::move(fn)) {}

    ~ScopeGuardForNewException() noexcept(executeOnException) {
        if (executeOnException == ec_.isNewUncaughtException()) {
            function_();
        }
    }
};
Собственно, все интересное сосредоточено в деструкторе, именно там сравнивается состояние счетчика исключений с шаблонным параметром и принимается решение вызывать или нет внутренний функтор. Обратите еще внимание как тот же шаблонный параметр изящно определяет сигнатуру деструктора: noexcept(executeOnException), поскольку SCOPE_FAIL должен быть exception safe, а SCOPE_SUCCESS вполне себе может выбросить исключение напоследок, чисто из вредности. По моему мнению, именно такие мелкие архитектурные детали делают C++ именно тем языком который я люблю.
Дальше все становится тривиальным, подобно SCOPE_EXIT мы определяем новый макрос:
enum class ScopeGuardOnFail {};
template <typename FunctionType>
ScopeGuardForNewException<
    typename std::decay<FunctionType>::type, true>
    operator+(detail::ScopeGuardOnFail, FunctionType&& fn) {
        return ScopeGuardForNewException<
            typename std::decay<FunctionType>::type, true
        >(std::forward<FunctionType>(fn));
    }

#define SCOPE_FAIL \
    auto ANONYMOUS_VARIABLE(SCOPE_FAIL_STATE) \
    = ::detail::ScopeGuardOnFail() + [&]() noexcept
И аналогично для SCOPE_EXIT
Посмотрим как теперь будут выглядеть исходные примеры:
void copy_file_tr(const path& from, const path& to) {
    bf::path t = to.native() + ".deleteme";
    SCOPE_FAIL { ::remove(t.c_str()); };
    bf::copy_file(from, t);
    bf::rename(t, to);
}

void move_file_tr(const path& from, const path& to) {
    bf::copy_file_transact(from, to);
    SCOPE_FAIL { ::remove(to.c_str()); };
    bf::remove(from);
}
Код выглядит прозрачнее, более того, каждая строчка что-то значит. А вот пример использования SCOPE_SUCCESS, заодно и демонстрация почему этот макрос может бросать исключения:
int string2int(const string& s) {
    int r;
    SCOPE_SUCCESS { assert(int2string(r) == s); };
    ...
    return r;
}
Таким образом, совсем небольшой синтаксический барьерчик отделяет нас от того чтобы добавить к идиомам C++ еще одну — декларативный стиль.

Заключение от первого лица

Все это наводит на определенные мысли о том что нас может ждать в недалеком будущем. Мне прежде всего бросилось в глаза то что все ссылки в докладе далеко не новы. Например, SCOPE_EXIT присутствует в boost.1.38, то есть уже почти десять лет, а статья самого Александреску о ScopeGuard вышла в Dr.Dobbs аж в 2000м году. Хочу напомнить что Александреску имеет репутацию провидца и пророка, так созданная им как демонстрация концепции библиотека Loki легла в основу boost::mpl, а потом почти полностью вошла в новый стандарт и еще задолго до того фактически задала идиомы метапрограммирования. С другой стороны, сам Александреску последнее время в основном занимается развитием языка D где все три упомянутые конструкции — scope exit, scope success and scope failure являются частью синтаксиса языка и давно заняли в нем прочное место.
Еще один любопытный момент — доклад Эрика Ниблера на той же самой конференции называется Ranges for the Standard Library. Хочу напомнить что ranges — еще одна стандартная концепция языка D, дальнейшее развитие концепции итераторов. Более того, сам доклад — фактически перевод (с D на C++) замечательной статьи H.S.Teoh Component programming with ranges.
Таким образом, похоже что C++ начал активно включать в себя концепции других языков, которые впрочем он сам же и инициировал. В любом случае, грядущий C++-17 похоже не будет рутинным обновлением. Учитывая уроки истории, семнадцатый год скучным не бывает, запасаемся попкорном, ананасами и рябчиками.

Литература

Здесь просто собраны в одном месте ссылки уже включенные в пост.
  1. Оригинальный аудио доклад
  2. Ссылка на материалы CppCon 2015
  3. Слайды к докладу Александреску
  4. Ссылка на оригинальную статью о ScopeGuard 2000г
  5. Документация по boost::ScopeExit
  6. Предложение Herb Sutter по изменению uncaught_exception()
  7. Оригинальная статья по ranges в D, кому интересно, хорошее неформальное введение в один из аспектов этого языка