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