Поэтому в стандартной библиотеке С++ имеется корректный способ учесть возникновение исключений в таком контексте и сохранить их как часть ассоциированного результата.
4.2.4. Сохранение исключения в будущем результате
Рассмотрим следующий коротенький фрагмент. Если передать функции square_root()
значение -1
, то она возбудит исключение, которое увидит вызывающая программа:
double square_root(double x) {
if (x<0) {
throw std::out_of_range("x<0");
}
return sqrt(x);
}
А теперь предположим, что вместо вызова square_root()
в текущем потоке
double y = square_root(-1);
мы вызываем ее асинхронно:
std::future<double> f = std::async(square_root,-1);
double y = f.get();
В идеале хотелось бы получить точно такое же поведение: чтобы поток, вызывающий f.get()
, мог увидеть не только нормальное значение y
, но и исключение — как в однопоточной программе.
Что ж, именно так на самом деле и происходит: если функция, вызванная через std::async
, возбуждает исключение, то это исключение сохраняется в будущем результате вместо значения, а когда будущий результат оказывается готовым, вызов get()
повторно возбуждает сохраненное исключение. (Примечание: стандарт ничего не говорит о том, возбуждается ли исходное исключение или его копия; различные компиляторы и библиотеки вправе решать этот вопрос по-разному.) То же самое происходит, когда функция обернута объектом std::packaged_task
, — если при вызове задачи обернутая функция возбуждает исключение, то объект исключения сохраняется в будущем результате вместо значения, и это исключение повторно возбуждается при обращении к get()
.
Разумеется, std::promise
обеспечивает те же возможности в случае явного вызова функции. Чтобы сохранить исключение вместо значения, следует вызвать функцию-член set_exception()
, а не set_value()
. Обычно это делается в блоке catch
:
extern std::promise<double> some_promise;
try {
some_promise.set_value(calculate_value());
} catch (...) {
some_promise.set_exception(std::current_exception());
}
Здесь мы воспользовались функцией std::current_exception()
, которая возвращает последнее возбужденное исключение, но могли вызвать std::copy_exception()
, чтобы поместить в объект-обещание новое исключение, которое никем не возбуждалось:
some_promise.set_exception(
std::copy_exception(std::logic_error("foo"));
Если тип исключения заранее известен, то это решение гораздо чище, чем использование блока try/catch
; мы не только упрощаем код, но и оставляем компилятору возможности для его оптимизации.
Есть еще один способ сохранить исключение в будущем результате: уничтожить ассоциированный с ним объект std::promise
или std::packaged_task
, не вызывая функцию установки значения в случае обещания или не обратившись к упакованной задаче. В любом случае деструктор std::promise
или std::packaged_task
сохранит в ассоциированном состоянии исключение типа std::future_error
, в котором код ошибки равен std::future_errc::broken_promise
, если только будущий результат еще не готов; создавая объект-будущее, вы даете обещание предоставить значение или исключение, а, уничтожая объект, не задав ни того, ни другого, вы это обещание нарушаете. Если бы компилятор в этом случае не сохранил ничего в будущем результате, то ожидающие потоки могли бы никогда не выйти из состояния ожидания.
До сих пор мы во всех примерах использовали std::future
. Однако у этого шаблонного класса есть ограничения, и не в последнюю очередь тот факт, что результата может ожидать только один поток. Если требуется, чтобы одного события ждали несколько потоков, то придётся воспользоваться классом std::shared_future
.