Почему все функции-члены в std::atomic появляются как с volatile, так и без него?
Я заметил, что большинство функций-членов std::atomic<T>
типы объявляются дважды, один раз с volatile
модификатор и один раз без ( пример)). Я проверил исходный код реализации стандартной библиотеки G++ и обнаружил, что все они являются точными дубликатами, например:
bool
load(memory_order __m = memory_order_seq_cst) const noexcept
{ return _M_base.load(__m); }
bool
load(memory_order __m = memory_order_seq_cst) const volatile noexcept
{ return _M_base.load(__m); }
Я не смог найти ни одного примера, где volatile
вариант ведет себя иначе, чем не volatile
один, отличается по типу возврата или что-нибудь в этом роде.
Это почему? Я думал volatile
функция-член также может быть вызвана в объектах, которые не являются volatile
, Так декларируя и определяя std::atomic::load(...) const volatile noexcept
и т.д. должно быть достаточно.
Обновить:
Основываясь на комментариях, мой вопрос сводится к следующему: Можете ли вы привести пример, когда некоторые звонки с использованием volatile
экземпляр (не обязательно std::atomic
) будет генерировать другую сборку в двух следующих случаях,
каждая функция-член появляется с одним и тем же телом с и без
volatile
,только
volatile
вариант существует?
Это при условии, что компилятор может выполнить любую оптимизацию, которую позволяет стандарт (или просто самые высокие уровни оптимизации).
1 ответ
Вероятно, все это связано с тем, что volatile
есть, для этого см. этот ответ. Поскольку варианты использования довольно тонкие по сравнению с обычной разработкой приложений, поэтому обычно это никого не волнует. Я предполагаю, что у вас нет практического сценария, в котором вы задаетесь вопросом, следует ли применять эти изменчивые перегрузки. Затем я попытаюсь привести пример, в котором они могут вам понадобиться (не думайте, что он слишком реален).
volatile std::sig_atomic_t status = ~SIGINT;
std::atomic<int> shareable(100);
void signal_handler(int signal)
{
status = signal;
}
// thread 1
auto old = std::signal(SIGINT, signal_handler);
std::raise(SIGINT);
int s = status;
shareable.store(10, std::memory_order_relaxed);
std::signal(SIGINT, old);
// thread 2
int i = shareable.load(std::memory_order_relaxed);
memory_order_relaxed
гарантирует атомарность и последовательность изменений порядка, без побочных эффектов. volatile
нельзя переупорядочить с побочными эффектами. Тогда вот мы, в теме 2 вы можете получить shareable
равно 10, но статус все еще не SIGINT
, Однако, если вы установите квалификатор типа в volatile
из shareable
это должно быть гарантировано. Для этого вам понадобятся методы volatile
-qualified.
Зачем тебе вообще делать что-то подобное? Я мог бы подумать, что у вас есть старый код, использующий старый volatile
на основе материала, и вы не можете изменить его по той или иной причине. Трудно представить, но я предполагаю, что может возникнуть необходимость иметь какой-то гарантированный порядок между atomic
а также volatile
встроенная сборка. Суть в том, ИМХО, что когда это возможно, вы можете использовать новую атомарную библиотеку вместо volatile
объекты, в случае, если есть некоторые volatile
объекты, от которых вы не можете избавиться и которые хотите использовать atomic
объекты, то вам может понадобиться volatile
квалификатор для atomic
объекты должны иметь надлежащие гарантии упорядочения, для этого вам потребуется перегрузка.
ОБНОВИТЬ
Но если все, что я хотел, - это использовать атомарные типы как энергозависимые, так и энергонезависимые, почему бы просто не реализовать первые?
struct Foo {
int k;
};
template <typename T>
struct Atomic {
void store(T desired) volatile { t = desired; }
T t;
};
int main(int i, char** argv) {
//error: no viable overloaded '='
// void store(T desired) volatile { t = desired; }
Atomic<Foo>().store(Foo());
return 0;
}
То же самое было бы с load
и другие операции, потому что обычно это не тривиальные реализации, которые требуют оператора копирования и / или конструктора копирования (который также может volatile
или неvolatile
).