21

std::atomic has deleted copy assignment operators. Hence, the following results in a compiler error:

std::atomic<int> a1, a2;
a1 = a2; // Error

I think the motivation for the deleted operators is explained e.g. in this post. So far, so good. But I noticed, that adding volatile causes the code to compile suddenly (live on godbolt):

volatile std::atomic<int> a1, a2;
a1 = a2; // OK

I do not really require volatile variables for my project, so this is just out of curiosity: Is this an oversight in the C++ standard, or is this deliberate (why?)?

Note: I can get a compiler error by hacking the std::atomic definition, either by adding

atomic & operator=(const volatile atomic &) volatile = delete;

or by removing the conversion operator operator T() const volatile noexcept.

Sedenion
  • 5,421
  • 2
  • 14
  • 42
  • 2
    How do you even come to this? Never in a million years I would have tried to put `volatile` upfront. – Eldinur the Kolibri Jul 17 '23 at 12:57
  • 5
    If you look at the asm, we see it's calling `std::__atomic_base::operator int() const volatile` and `operator=(int)`. So it's converting to `int` and back, not actually doing copy-assignment directly between atomics. IDK why that can happen with `volatile` but not without, or if the ISO standard even allows it, but it would be surprising if all 3 major compilers had the same bug. (including clang with `-stdlib=libc++`, vs. gcc with `libstdc++` vs. MSVC's own headers.) – Peter Cordes Jul 17 '23 at 12:58
  • 9
    [LWG 908](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2992.htm#LWG908) explains _why_ adding the `volatile` qualifier induces the observed difference: the copy-assignment operator being `volatile` is kind of a hack to make the compiler prefer one conversion chain to another in `a1=0`. Now, has the committee foreseen the consequence OP found there? This is the question. – YSC Jul 17 '23 at 13:15
  • 2
    @EldinurtheKolibri Well, I found this interesting piece while specializing `std::atomic` for a custom type which implements some simple fixed point arithmetic. It is just a wrapper around an integer. I started the implementation by going through the cppreference documentation from the top. When implementing the assignments, I added a compile time unit test to check that the volatile assignment does NOT compile (simply via a `requires`). I was surprised when that very test failed later on, namely once I added the conversion `operator T() const volatile`, and got curious. Hence the question. – Sedenion Jul 17 '23 at 13:55

1 Answers1

9

This is LWG3633.


std::atomic<T> has a (deleted) copy assignment operator taking a const atomic<T>& (1), an assignment operator function taking a T (2), and a (non-explicit) conversion function to T (3):

// (1)
atomic& operator=(const atomic&) = delete;
atomic& operator=(const atomic&) volatile = delete;

// (2)
T operator=(T) noexcept;
T operator=(T) volatile noexcept;

// (3)
operator T() const noexcept;
operator T() const volatile noexcept;

When the assignment source is a non-volatile std::atomic<T>, both assignment operator functions are viable, but (1) is preferred because it does not require a user-defined conversion on the right operand.

When the right operand is volatile, (1) is not viable because const atomic<T>& cannot bind to a volatile glvalue, so (2) is chosen.

duck
  • 1,455
  • 2
  • 8