Для тех, кто незнаком с историей вопроса, рассмотрим класс stack
. Вектор — это контейнер с динамически изменяемым размером, поэтому при копировании вектора библиотека должна выделить из кучи память. Если система сильно загружена или имеются жесткие ограничения на ресурсы, то операция выделения памяти может завершиться неудачно, и тогда копирующий конструктор вектора возбудит исключение std::bad_alloc
. Вероятность такого развития событий особенно велика, если вектор содержит много элементов. Если бы функция pop()
возвращала вытолкнутое из стека значение, а не только удаляла его из стека, то мы получили бы потенциальную проблему: вытолкнутое значение возвращается вызывающей программе только после модификации стека, но в процессе копирования возвращаемых данных может возникнуть исключение. Если такое случится, то только что вытолкнутые данные будут потеряны — из стека они удалены, но никуда не скопированы! Поэтому проектировщики интерфейса std::stack
разбили операцию на две: получить элемент, находящийся на вершине (top()
), а затем удалить его из стека (pop()
). Теперь, данные, которые не удалось скопировать, остаются в стеке; если проблема связана с нехваткой памяти в куче, то, возможно, приложение сможет освободить немного памяти и попытаться выполнить операцию еще раз.
Увы, это как раз то разбиение, которого мы пытались избежать в попытке уйти от гонки! К счастью, альтернативы имеются, но они не бесплатны.
Первый вариант решения — передавать функции pop()
ссылку на переменную, в которую она должна будет поместить вытолкнутое из стека значение:
std::vector
some_stack.pop(result);
Во многих случаях это приемлемо, но есть и очевидный недостаток: вызывающая программа должна до обращения к функции сконструировать объект того типа, которым конкретизирован стек, чтобы передать его в качестве аргумента. Для некоторых типов это не годится, так как конструирование дорого обходится с точки зрения времени или потребления ресурсов. Для других типов это вообще может оказаться невозможно, так как конструкторы требуют параметров, которые в данной точке программы могут быть недоступны. Наконец, требуется, чтобы хранящийся в стеке тип допускал присваивание. Это существенное ограничение, многие пользовательские типы не поддерживают присваивание, хотя могут поддерживать конструирование перемещением и даже копированием (и потому допускают возврат по значению).
Проблема с безопасностью относительно исключений в варианте функции pop(), возвращающей значение, проявляется только тогда, когда исключение может возникать в процессе возврата значения. Во многих типах имеются копирующие конструкторы, которые не возбуждают исключений, а после поддержки в стандарте С++ ссылок на r-значения (см. приложение А, раздел А.1), появилось еще много типов, в которых перемещающий конструктор не возбуждает исключений, даже если копирующий конструктор может их возбуждать. Один из вариантов решения заключается в том, чтобы наложить на потокобезопасный стек ограничение: в нем можно хранить только типы, поддерживающие возврат по значению без возбуждения исключений.
Это решение, пусть и безопасное, не идеально. Хотя на этапе компиляции можно узнать, существует ли копирующий или перемещающий конструктор, который не возбуждает исключений, — с помощью концепций std::is_nothrow_copy_constructible
, std::is_nothrow_move_constructible
и характеристик типов, но это слишком ограничительное требование. Пользовательских типов, в которых копирующий конструктор может возбуждать исключение и перемещающего конструктора нет, гораздо больше, чем типов, в которых копирующий и (или) перемещающий конструктор гарантированно не возбуждают исключений (хотя ситуация может измениться, когда разработчики привыкнут к появившейся в С++11 поддержке ссылок на r-значения). Было бы крайне нежелательно запрещать хранение таких объектов в потокобезопасном стеке.