Что такое "исключение рвота"?

Недавно на CodeReview.SE я наткнулся на ответ, в котором говорится о технике, называемой "исключение рвоты". По-видимому, этот трюк используется для использования того, что исключения должны быть реализованы поточно-безопасным образом независимо от того, поддерживает ли компилятор переменные thread_local.

Вставьте часть этого ответа ниже:

Существует существующая методика, которая не является разнородной, называемой "исключающей рвотой". Обратите внимание:

void f(void(*p)()) {
    p();
}
template<typename F> void real_f(F func) {
    try {
        throw func;
    } catch(...) {
        f([] {
            try {
                throw;
            } catch(F func) {
                func();
            }
        });
    }
}

Это нарушает тот факт, что компилятор должен предоставить поток-локальный стек для сложных объектов для использования в качестве хранилища исключений, независимо от их поддержки других локальных функций потока, и, следовательно, пользуется широкой поддержкой компилятора. Наиболее очевидные недостатки: a) это ужасно, и b) он ограничен семантикой стека.

Мой вопрос в том, как этот трюк действительно работает и безопасен ли он?

Ответ 1

Этот метод основан на том, что исключения должны быть реализованы поточно-безопасным способом, чтобы исключения могли использоваться в многопоточном приложении. Даже pre С++ - 11 компиляторы поддерживали потокобезопасные исключения до того, как потоки стали частью стандарта С++.

Каждый поток throw/catch исключений независимо от других потоков, используя хранилище, зависящее от потока, для хранения исключений. throw без аргумента перерисовывает текущую исключение, хранящуюся в этом потоковом хранилище. Это хранилище для исключения используется для хранения функции с ее захваченными аргументами (с учетом состояния лямбда или любого другого вызываемого).

Недостатком этого метода является то, что выбрасывание исключения обычно связано с распределением памяти, поэтому оно добавляет накладные расходы на вызовы new/delete.


Другой способ реализовать это - использовать не переносимый, но широко поддерживаемый спецификатор хранилища __thread. Это позволяет избежать накладных расходов на распределение динамической памяти:

void f(void(*p)()) { // The C-style function.
    p();
}

__thread void(*function)();

template<class Function>
void adapter() {
    (*reinterpret_cast<Function*>(function))();
}

template<typename F>
void invoke_f(F const& func) {
    function = reinterpret_cast<void(*)()>(&func);
    f(adapter<F const>);
}

int main(int ac, char**) {
    invoke_f([ac]{ std::cout << ac << '\n'; });
}