Как стать автором
Обновить

Комментарии 36

А зачем boost, если в C++11 есть std::function?
Как-то совсем забыл что function тоже добавили в новый стандарт, сейчас поменяю.
Здорово, но пока нет уверенности в корректной обработке эксепшнов, неюзабельно, если надумаете, как поправить, будет здорово, самому этого не хватает в C++.
Что-то мне подсказывает, что тут её и не может быть. Ну разве что в деструкторе вызов функтора обложить try-catch'ем, и не выпускать возможные исключения за пределы деструктора.
Мне так видится, что закрывающий код вообще не должен бросать эксепшены, та же проблема может быть в любом деструкторе. Можно, конечно, вызывать этот блок в пустом try..catch, вот только это совсем не корректно.
Зато безопасно. :)
В деструкторе бросать наверно таки не хорошо.
Но код, который будет выполнятся при выходе из функции, не обязательно будет заниматся очисткой ресурсов.
И вот тут то лучше не ловить эксепшены, ибо черезмерное увлечение перехватом всего и вся потом выливается в долгую и геморойную отладку, когда приложение вроде и не валится, но работает как-то не так.
Ну разве что в какой-то области, где подвергаются опасности жизни людей — лучше перехватить лишний раз исключение и как-то попытатся продолжить работу, чем упасть.

Вобщем надо ли там оборачивать в try/catch — сильно зависит от того, что там делается.
Как вариант, можно завести дав типа класса — один безопасный — с обёртками, второй не безопасный.
Когда-то давно написал:
class call_func_when_out_of_scope {
 public:
  call_func_when_out_of_scope(boost::function<void()> func_to_call_in_destructor)
      : f(func_to_call_in_destructor)
  {}

  ~call_func_when_out_of_scope() {
    if (!f.empty())
      f();
  }
 protected:
  boost::function<void()> f;
};



Использовать, например так:
void test(int i)
{
  printf("in test, i=%d!\n", i);
}

int main()
{
  call_func_when_out_of_scope h(boost::bind(test, 42));

  printf("out of main()!\n");
  return 0;
}
/* Output:
out of main()!
in test, i=42!
*/



Или так:
{
  ...
  if (FILE* f = fopen("foo_bar.data", "r")) {
    call_func_when_out_of_scope close_file(boost::bind(fclose, f));
    ....
  }
}

Недостаток вашей реализации в том, что вызвать можно только какую-то отдельную функцию, в моей-же можно вызвать просто произвольный код. Хотя некоторые вещи, такие как ваш второй пример, всё-же удобнее делать вашей.
Какие проблемы? Код в лямбду заворачивать — и вперёд. Функтор он и в Африке функтор.
Ну в таком случае наши решения весьма похожи ), отличие только в макросах, которые добавляют немного удобства.
Вот конкретно вашу реализацию я бы не стал использовать в своих проектах: в ней, во-первых, «голые new/delete» (что сразу вызывает опасения), во-вторых, вызовы виртуальных функций (хотя реализация всегда существует в одном-единственном виде, откуда тут полиморфизм взялся?).

Это сразу вызывает вопросы, а для «маленьких утилитарных классов» это недопустимо.
Если вы дочитали статью до конца, то могли увидеть что в последней версии, ни new, ни delete, ни виртуализация напрямую не используется.
Другое дело что, если мне не изменяется память, тот же std::function именно через виртуализацию и сделан.
Нет, там «compile time polymorphism», через шаблоны.
Можете рассказать на пальцах, как там в компайл-тайм (т.е. без вирт. функций и указателей на функции) реализован вызов объектов, конкретный тип которых неизвестен коду, вызывающему () для объекта function<>?

Например для случая:
// module 1
void fun()
{
}
struct str
{
    void operator()() {}
};

std::function<void()> f1 = fun;
std::function<void()> f2 = str();

// module 2
f1();
f2();
Я, строго говоря, неправ был. На MSVC реализация сделана через базовый класс с виртуальным _Do_call, а в GCC — через указатель на функцию шаблонного класса. Слова virtual эта реализация не содержит, но по сути это тоже самое.
Например, так:
  call_func_when_out_of_scope h2([]() {
    printf("in inner lambda\n");
  });

Опять же. Если мы говорим о C++11 то, мне кажется, есть более простой вариант без использования рантайм-полиморфизма:

template<typename F>
class ScopeExit
{
public:
    ScopeExit(const F& fn) : m_fn(fn) {;}
    ~ScopeExit()
    {
        m_fn();
    }
private:
    F m_fn;
};

template<typename F>
ScopeExit<F> CreateScopeExit(F fn)
{
    return ScopeExit<F>(fn);
}

#define EXIT_SCOPE_CREATE_UNIQ_NAME2(line) exit_scope_guard_##line
#define EXIT_SCOPE_CREATE_UNIQ_NAME(line) EXIT_SCOPE_CREATE_UNIQ_NAME2(line)
#define EXIT_SCOPE(F) const auto  &EXIT_SCOPE_CREATE_UNIQ_NAME(__LINE__) = CreateScopeExit([]{F;})


Варианты использования:
int main()
{
    std::cout << "Outer scope enter" << std::endl;
    EXIT_SCOPE(std::cout << "Outer scope exited" << std::endl);

    {
        std::cout << "Inner scope enter" << std::endl;

        EXIT_SCOPE({std::cout << "Inner scope exited" << std::endl;});
        std::cout << "Inner scope exit" << std::endl;
    }

    std::cout << "Outer scope exit" << std::endl;
    return 0;
}
То-же думал над такой реализацией, но хотелось обойтись без дополнительных скобочек. Хотя с точки зрения производительности ваш код быстрее и лучше поддаётся оптимизации
Есть, кстати, возможность доточить этот код до вариант «без дополнительных скобочек».
Можно пример? А то у меня как-то нет вариантов.
Всё достаточно просто:
class ScopeExitCreator
{
public:
    template<typename Fn>
    ScopeExit<Fn> operator << (const Fn &fn)
    {
        return ScopeExit<Fn>(fn);
    }
};

#define EXIT_SCOPE_CREATE_UNIQ_NAME2(line) exit_scope_guard_##line
#define EXIT_SCOPE_CREATE_UNIQ_NAME(line) EXIT_SCOPE_CREATE_UNIQ_NAME2(line)
#define EXIT_SCOPE const auto  &EXIT_SCOPE_CREATE_UNIQ_NAME(__LINE__) = ScopeExitCreator() << [&]()

(класс ScopeExit тот же)
Ну и использование:
int main()
{
    std::cout << "Outer scope enter" << std::endl;
    EXIT_SCOPE{std::cout << "Outer scope exited" << std::endl;};

    {
        std::cout << "Inner scope enter" << std::endl;

        EXIT_SCOPE{std::cout << "Inner scope exited" << std::endl;};
        std::cout << "Inner scope exit" << std::endl;
    }

    std::cout << "Outer scope exit" << std::endl;
    return 0;
}


Но тут есть одна тонкость связанная больше со стилем кодирования. Декларирования макросов без параметров лучше всё-таки избегать. Резко увеличивается вероятность неожиданной коллизии.
Чисто теоретический вопрос:

// 1
exit_scope guard1 = [&](){ std::cout
вот блин, стёрлось даже с использованием <source>

вопрос:
// 1
exit_scope guard1 = [&](){ std::cout<< «First exit scope „<<omega<<std::endl; };
// 2
exit_scope guard1 = [&](){ std::cout<< “First exit scope „<<omega<<std::endl; };

В случаях (1) и (2) будет сгенерирован разный код полностью для exit_scope или только для struct caller?
Что-то туплю уже на ночь глядя.
//2
exit_scope guard1 = [](void* a){ std::cout<< “First exit scope „<<reinterpret_cast(a)<<std::endl; };

Конечно разный exit_scope
В Вашем коде есть один существенный изъян.
Для того, чтобы код в EXIT_SCOPE выполнялся всегда и самым последним в функции, этот макрос необходимо
писать в самом начале этой функции, и обязательно до вызова любого return или кода, который может бросить exception, иначе EXIT_SCOPE вообще не выполнится. Но в таком случае переменные функции, которые были объявлены позже будут не доступны внутри лямбды.

int test1()
{
    std::string omega = "very big string"; // throw bad_alloc, EXIT_SCOPE не выполнится
    if (omega.empty())
    {
         return -1; //EXIT_SCOPE не выполнится, это как минимум не очевидно
    }
    std::cin >> omega;
    EXIT_SCOPE{ std::cout << "Second exit scope" << omega<< std::endl; };
    EXIT_SCOPE{ std::cout << "Third exit scope" << omega<<std::endl; };
    return 0;
}
Ну мне кажется, что это скорее фича, чем баг.
В этом есть логика — выражение не выполнится, покаместь оно не объявлено.
Я правда не знаю, как в этом случаи поведёт себя тот-же D
Ну ничего-себе фитча, Ваш код может ввести в заблуждение и не освободить к примеру память, или не освободить ресурсы и этим вызвать зависание программы, либо вообще падение и порчу пользовательских данных.

Возникает большой соблазн написать EXIT_SCOPE внизу функции, т.к. это логичнее как-то, типа finaly блок перед выходом, да и в конце функции доступны все необходимые переменные. А иногда вообще нельзя будет написать такой блок вначале функции из-за недоступности каких-либо переменных, и такой блок на автомате перенесут в середину или в конец функции.

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

Проектировать библиотечный код необходимо таким образом, чтобы избегать всевозможных ошибок.
А в Вашем случае макрос еще и прячет scope объект, что приводит к полному непониманию происходящего и по этому обязательно приведет к ошибкам.
А как, по вашему, это должно работать, чтобы ещё и в правила и соглашения языка C++ укладываться? Пока то, что вы написали, похоже примерно на следующее: написать в конце метода main кучу вызовов библиотечной функции atexit, а потом удивляться — почему ничего не работает…
Я лишь хочу донести мысль о том, что использование предложенного макроса EXIT_SCOPE в таком виде, как он представлен неизбежно приведет к ошибкам и ухудшит сопровождение кода.

Представьте себе ситуацию, когда не особо выдающийся программист Вася вносит дополнительную проверку в чужой код перед этим макросом и вызывает return, либо throw.
При том ошибки, допущенные из-за использования такого подхода будут проявляться редко и будут трудно воспроизводимы.

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

Возможно стоит ввести дополнительный объект, который нужно будет объявлять в начале функции, и именно при уничтожении этого объекта вызывать функтор, объявленный позже, но тут опять таки есть свои ньюансы…

Лично я лучше сделаю отдельный RAI объект для освобождения памяти или ресурсов, ну или воспользуюсь существующим unique_ptr, чем буду городить нечто подобное этому макросу.
Я ведь не зря привёл пример с atexit. Да и объект в начале функции — тоже не поможет. Сейчас SCOPE_EXIT подчиняется общим правилам control flow программы. Если выполнение до него не дошло — ничего не вызовется. Если дошло — то вызовется. Изменять эту логику на что-то более хитрое — вот это то как раз опасно, потому что появится сущность, которая «волшебным образом» будет работать по другим правилам. Чем проще контракт — тем проще его понять, и проще пользоваться.
О чем вообще можно говорить,
когда автор статьи не потрудился привести пример,
демонстрирующий работу своего макроса.
(или сам-же наступил на свои грабли?)

Давайте разберем его пример:
int test1()
{
    std::string omega;
    std::cin >> omega;
    EXIT_SCOPE{ std::cout << "Second exit scope" << omega<< std::endl; };
    EXIT_SCOPE{ std::cout << "Third exit scope" << omega<<std::endl; };
    return 0;
}

Этот код эквивалентен коду вообще без макроса:
int test1()
{
    std::string omega;
    std::cin >> omega;
    std::cout << "Second exit scope" << omega<< std::endl;;
    std::cout << "Third exit scope" << omega<<std::endl;;
    return 0;
}

Чтобы заработал макрос нужно его написать перед вызовом основного кода:
int test1()
{
    std::string omega;
    EXIT_SCOPE{ std::cout << "Second exit scope" << omega<< std::endl; };
    EXIT_SCOPE{ std::cout << "Third exit scope" << omega<<std::endl; };
    std::cin >> omega; // can throw an exception
    omega += " super puper omega"; // throw bad_alloc()
    //и еще куча кода
    if (omega.empty())
    {
        return -1;
    }
    return 0;
}

А затем приходит программист Вася и «исправляет» код:
int test1()
{
    std::string omega;
    std::cin >> omega; // can throw an exception
    omega += " super puper omega"; // throw bad_alloc()
    //и еще куча кода
    if (omega.empty())
    {
        return -1;
    }
    std::string alpha;
    std::cin >> alpha;
    EXIT_SCOPE{ std::cout << "Second exit scope" << omega << alpha << std::endl; };
    EXIT_SCOPE{ std::cout << "Third exit scope" << omega<< alpha << std::endl; };
    return 0;
}

Может быть автор потрудится предоставить более наглядный пример из жизни, где было бы задействовано что-то более реальное чем вывод на консоль?
Думаю, это очевидно, что основной сценарий использования EXIT_SCOPE такой же, как и метода atexit. Если разработчик не понимает, зачем нужен инструмент, то, очевидно, незачем этот инструмент использовать. И, кстати, ваши первые два примера кода не эквивалентны друг другу.
В первом примере сделайте так:

exit_scope guard1([&]() {
    std::cout<< "First exit scope " << omega << std::endl;
});


По крайней мере GCC не будет ругаться на закрытый конструктор копирования.
не знаю на сколько это чисто g++ фича, но заметил, что

SomeClass::some_func()
{

otherclass->set_callback([&this](){ ....; this->protected_method(); }

}

OtherClass::call_callback()
{
callback(); // который принят в set_callback(std::function);
}

работает без варнингов.
Фактически обращение через замыкание к закрытым методам.
а ещё мне интересно, почему у меня не работает <source lang=«cpp»>… </source>
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации