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.