Третий вариант — возвращать не копию вытолкнутого элемента по значению, а указатель на него. Его достоинство в том, указатели можно копировать, не опасаясь исключений, поэтому указанную Каргиллом проблему мы обходим. А недостаток в том, что возврат указателя заставляет искать средства для управления выделенной объекту памятью, так что для таких простых типов, как целые числа, накладные расходы на управление памятью могут превысить затраты на возврат типа по значению. В любом интерфейсе, где применяется этот вариант, в качестве типа указателя было бы разумно избрать std::shared_ptr
; мало того что это предотвращает утечки памяти, поскольку объект уничтожается вместе с уничтожением последнего указателя на него, так еще и библиотека полностью контролирует схему распределения памяти и не требует использования new
и delete
. Это существенно с точки зрения оптимизации — требование, чтобы память для всякого хранящегося в стеке объекта выделялась с помощью new
, повлекло бы заметные накладные расходы по сравнению с исходной версией, небезопасной относительно потоков.
Никогда не следует пренебрегать гибкостью, особенно в обобщенном коде. Если остановиться на варианте 2 или 3, то будет сравнительно нетрудно реализовать и вариант 1, а это оставит пользователю возможность выбрать наиболее подходящее решение ценой очень небольших накладных расходов.
Пример определения потокобезопасного стека
В листинге 3.4 приведено определение класса стека со свободным от гонок интерфейсом. В нем реализованы приведенные выше варианты 1 и 3: имеется два перегруженных варианта функции-члена pop()
— один принимает ссылку на переменную, в которой следует сохранить значение, а второй возвращает std::shared_ptr<>
. Интерфейс предельно прост, он содержит только функции: push()
и pop()
.
Листинг 3.4. Определение класса потокобезопасного стека
#include
struct empty_stack: std::exception {
const char* what() const throw();
};
template
class threadsafe_stack {
public:
threadsafe_stack();
threadsafe_stack(const threadsafe_stack&);
threadsafe_stack& operator=(const threadsafe_stack&)
= delete;←
(1)
void push(T new_value);
std::shared_ptr
void pop(T& value);
bool empty() const;
};
Упростив интерфейс, мы добились максимальной безопасности — даже операции со стеком в целом ограничены: стек нельзя присваивать, так как оператор присваивания удален (1) (см. приложение А, раздел А.2) и функция swap()
отсутствует. Однако стек можно копировать в предположении, что можно копировать его элементы. Обе функции pop()
возбуждают исключение empty_stack
, если стек пуст, поэтому программа будет работать, даже если стек был модифицирован после вызова empty()
. В описании варианта 3 выше отмечалось, что использование std::shared_ptr
позволяет стеку взять на себя распределение памяти и избежать лишних обращений к new
и delete
. Теперь из пяти операций со стеком осталось только три: push()
, pop()
и empty()
. И даже empty()
лишняя. Чем проще интерфейс, тем удобнее контролировать доступ к данным — можно захватывать мьютекс на все время выполнения операции. В листинге 3.5 приведена простая реализация в виде обертки вокруг класс std::stack<>
.
Листинг 3.5. Определение класса потокобезопасного стека
#include
#include
#include
#include
struct empty_stack: std::exception {
const char* what() const throw();
};
template
class threadsafe_stack {
private:
std::stack
mutable std::mutex m;
public:
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other) {
std::lock_guard
data = other.data; ←┐
(1) Копирование производится в теле
} │
конструктора
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value) {
std::lock_guard
data.push(new_value);
}
std::shared_ptr
Перед тем как выталкивать значение,
{ ←┘
проверяем, не пуст ли стек