Почему Александреску не может использовать std::uncaught_exception() для реализации SCOPE_FAIL в ScopeGuard11?

Без сомнения, многие люди знакомы с шаблоном ScopeGuard мистера Александрескуса (теперь часть Loki) и новой версией ScopeGuard11, представленной здесь: http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C

с исходным кодом здесь: https://gist.github.com/KindDragon/4650442

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

Поскольку я ни в коем случае не такой гений или опытный, как г-н Александреску, я ожидаю, что реализовать SCOPE_FAIL будет не так просто:

~ScopeGuard11(){                      //destructor
    if(std::uncaught_exception()){    //if we are exiting because of an exception
        f_();                         //execute the functor
    }
    //otherwise do nothing
}

Мой вопрос: почему бы и нет?


person odinthenerd    schedule 17.02.2013    source источник
comment
Странно, что-то мне подсказывает, что должно работать, но если я попробую, то uncaught_exception() всегда возвращает false.   -  person Andy Prowl    schedule 18.02.2013
comment
Я смутно припоминаю, что у Херба Саттера было что-то подобное на GotW когда-то, но я больше не могу его найти. Может Альцгеймер ;) или я не то гуглю.   -  person odinthenerd    schedule 18.02.2013
comment
Я думаю, что в случае с защитой области вы могли бы использовать std::uncaught_exception, поскольку защита области никогда не будет членом другого класса (и, конечно, не локальной переменной в деструкторе какого-либо класса).   -  person Xeo    schedule 18.02.2013
comment
@Xeo: по-прежнему std::uncaught_exception() кажется, что все время возвращает false. Возможно это баг, или я что-то упускаю из виду?   -  person Andy Prowl    schedule 18.02.2013
comment
Я не видел разговора - как SCOPE_FAIL относится к коду, который вы разместили?   -  person Kerrek SB    schedule 18.02.2013
comment
@Xeo: Боже. Я так сильно сосать.   -  person Andy Prowl    schedule 18.02.2013
comment
@PorkyBrain: gotw.ca/gotw/047.htm ?   -  person Lightness Races in Orbit    schedule 18.02.2013
comment
@AndyProwl Вы не можете использовать его в блоке catch, так как исключение уже перехвачено. Вы можете использовать его только в деструкторе.   -  person JiaHao Xu    schedule 09.10.2018


Ответы (1)


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

Попробуйте этот пример:

#include <exception>
#include <iostream>
#include <string>

// simplified ScopeGuard11
template <class Fun>
struct ScopeGuard11 {
     Fun f_;
     ScopeGuard11(Fun f) : f_(f) {}
     ~ScopeGuard11(){                      //destructor
        if(std::uncaught_exception()){    //if we are exiting because of an exception
            f_();                         //execute the functor
         }
         //otherwise do nothing
      }
};

void rollback() {
  std::cout << "Rolling back everything\n";
}
void could_throw(bool doit) {
  if (doit) throw std::string("Too bad");
}

void foo() {
   ScopeGuard11<void (*)()> rollback_on_exception(rollback);
   could_throw(false);
   // should never see a rollback here
   // as could throw won't throw with this argument
   // in reality there might sometimes be exceptions
   // but here we care about the case where there is none 
}

struct Bar {
   ~Bar() {
      // to cleanup is to foo
      // and never throw from d'tor
      try { foo(); } catch (...) {}
   }
};

void baz() {
   Bar bar;
   ScopeGuard11<void (*)()> more_rollback_on_exception(rollback);
   could_throw(true);
}

int main() try {
   baz();
} catch (std::string & e) {
   std::cout << "caught: " << e << std::endl;
}

Вы хотели бы видеть один rollback при выходе из baz, но вы увидите два, в том числе ложный при выходе из foo.

person JoergB    schedule 18.02.2013
comment
Интересно. Мне интересно, однако, если это не вопрос хорошего дизайна, чтобы избежать этого шаблона. Здесь foo() никогда не будет выбрасывать (иначе было бы плохо вызывать его из деструктора). Но если он никогда не выбросит, зачем использовать откат при исключении? - person Andy Prowl; 18.02.2013
comment
То, что foo() было настроено так, чтобы никогда не бросать, было сделано для демонстрации. В реальном проблемном случае условия, когда could_throw может бросить (sic!), были бы менее очевидны. В этом случае деструктор Bar может выполнить try { foo(); } catch (...) {}. Или между ними может быть больше слоев, чтобы любое исключение в foo() не покидало деструктор Bar. Тем не менее, вы получите ложный откат в foo(), даже если он будет оставлен обычным возвратом. - person JoergB; 18.02.2013
comment
@AndyProwl: я изменил пример, чтобы сделать его более понятным. - person JoergB; 18.02.2013
comment
Имеет смысл. Однако в этом случае конструктор ScopeGuard11 может проверить, возвращает ли uncaught_exception() true, и в этом случае сохранить результат std::current_exception(). Затем в деструкторе он снова вызовет std::current_exception() и сравнит результат с ранее сохраненным указателем исключения. При равенстве ничего не будет. Если отличается, он вызовет функцию отката. Имеет ли это смысл? - person Andy Prowl; 18.02.2013
comment
Хорошо, забудьте об этом, это не сработает: std::current_exception() вернет nullptr до того, как исключение будет обработано. +1ред. - person Andy Prowl; 18.02.2013
comment
Я вижу, что деление кода на то, что его можно или нельзя запускать как часть очистки, проблематично. Я предполагаю, что нет способа обеспечить синтаксический вызов this из политики деструктора? Например, флаг InDestructor или что-то в этом роде? - person odinthenerd; 18.02.2013
comment
@AndyProwl Можно ли заставить это работать, если uncaught_exception вернет exception_ptr вместо bool? ISTM, который будет совместимым изменением; было бы это осуществимо? - person ecatmur; 18.02.2013
comment
@ecatmur: Вероятно, да, но для этого также потребуется изменить current_exception(). В настоящее время current_exception() возвращает nullptr перед передачей управления обработчику исключений. Тем не менее, я все еще думаю, что решение OP приемлемо, если вы должным образом позаботитесь о том, чтобы не вызывать функции с защитой от исключений, когда исключение было создано и еще не обработано. Я бы сказал, что конструктор ScopeGuard11 может даже утверждать (или сам генерировать исключение), если uncaught_exception() возвращает true. Но я просто думаю вслух, не уверен, что это хорошая идея. - person Andy Prowl; 18.02.2013
comment
@AndyProwl Это потребует еще больших изменений в том, как работает поддержка исключений. current_exception может каждый раз создавать новую копию внутреннего исключения, поэтому вы вообще не можете полагаться на идентификатор объекта исключения. - person JoergB; 18.02.2013
comment
@JoergB: Правильно. 18.8.5/8: Не указано, относятся ли возвращаемые значения двух последовательных вызовов current_exception к одному и тому же объекту исключения. [Примечание. То есть не указано, создает ли current_exception новую копию при каждом вызове. —конец примечания] - person Andy Prowl; 18.02.2013
comment
@AndyProwl, утверждающий конструктор защиты области действия - это то, о чем я тоже думал, проблема в том, что утверждение будет показывать себя только в том случае, если деструктор действительно вызывается из-за раскручивания стека, если в этом контексте очень редко выдается исключение ошибка ( или неправильное использование) могут оставаться незамеченными в течение очень долгого времени. - person odinthenerd; 18.02.2013
comment
@AndyProwl Я думаю, что этот класс будет работать, если при построении проверять uncaught_exception() == false. Если он возвращает true, я не знаю, что делать... throw std::logic_error( "Exception unwinding is active!" );? - person Charles L Wilcox; 27.03.2013
comment
@AndyProwl Не для саморекламы, но я предложил это как возможное решение моего собственного вопроса в Вопрос 15504166. - person Charles L Wilcox; 27.03.2013