Помимо обычного набора операций (load(), store(), exchange(), compare_exchange_weak() и compare_exchange_strong()), атомарные целочисленные типы такие, как std::atomic или std::atomic) обладают целым рядом дополнительных операций: fetch_add(), fetch_sub(), fetch_and(), fetch_or(), fetch_xor(), их вариантами в виде составных операторов присваивания (+=, -=, &=, |=, ^=) и операторами пред- и постинкремента и декремента (++x, x++, --x, x--). Это не весь набор составных операторов присваивания, имеющихся у обычного целочисленного типа, но близко к тому — отсутствуют лишь операторы умножения, деления и сдвига. Поскольку атомарные целочисленные значения обычно используются в качестве счетчиков или битовых масок, потеря не слишком велика, а в случае необходимости недостающие операции можно реализовать с помощью вызова функции compare_exchange_weak() в цикле.
Семантика операций близка к семантике функций fetch_add() и fetch_sub() в типе std::atomic; именованные функции выполняют свои операции атомарно и возвращают ++x увеличивает значение переменной на единицу и возвращает новое значение, а x++ увеличивает значение переменной на единицу и возвращает старое значение. Как вы теперь уже понимаете, результатом в обоих случаях является значение ассоциированного целочисленного типа.
Мы рассмотрели все простые атомарные типы; остался только основной обобщенный шаблон класса std::atomic<> без специализации.
5.2.6. Основной шаблон класса std::atomic<>
Наличие основного шаблона позволяет создавать атомарные варианты пользовательских типов, в дополнение к стандартным атомарным типам. Однако в качестве параметра шаблона std::atomic<> может выступать только тип, удовлетворяющий определенным условиям. Точнее, чтобы тип UDT мог использоваться в конструкции std::atomic, в нем должен присутствовать memcpy() или эквивалентную ей, поскольку исполнять написанный пользователем код не требуется.
Наконец, тип должен допускать memcpy(), но и сравнивать их с помощью memcmp(). Это необходимо для правильной работы операции сравнить-и-обменять.
Чтобы понять, чем вызваны такие ограничения, вспомните рекомендацию из главы 3: не передавать ссылки и указатели на защищенные данные за пределы области видимости в виде аргументов предоставленной пользователем функции. В общем случае компилятор не в состоянии сгенерировать свободный от блокировок код для типа std::atomic, поэтому он вынужден применять внутренние блокировки. Если бы пользовательские операторы присваивания и сравнения были разрешены, то пришлось бы передавать ссылку на защищенные данные в пользовательскую функцию, нарушая тем самым приведённую выше рекомендацию. Кроме того, библиотека вправе использовать единую блокировку для всех нуждающихся в ней атомарных операций, поэтому, разрешив вызывать пользовательские функции в момент, когда эта блокировка удерживается, мы могли бы получить взаимоблокировку или надолго задержать другие потоки, если сравнение занимает много времени. Наконец, эти ограничения повышают шансы на то, что компилятор сумеет сгенерировать для std::atomic код, содержащий истинно атомарные команды (и тем самым обойтись в данной конкретизации вообще без блокировок), поскольку в этой ситуации он вправе рассматривать определенный пользователем тип как неструктурированную последовательность байтов.