8

I noticed that most member functions of std::atomic<T> types are declared twice, once with the volatile modifier and once without (example)). I checked the source code of the G++ standard library implementation and I found all of them to be exact duplicates, for example,

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); }

I could not find any example where the volatile variant behaves differently than the non-volatile one, differs in return type or anything of that sort.

Why is that? I thought a volatile member function could also be called in objects which are not volatile. So declaring and defining std::atomic::load(...) const volatile noexcept etc. should be enough.

Update:

Based on the comments my question basically boils down to: Can you provide an example where some calls using a non-volatile instance (not necessarily of std::atomic) would generate a different assembly in the two following cases,

  1. every member function appears with the same body with and without volatile,

  2. only the volatile variant exists?

This, assuming the compiler can do whatever optimization the standard allows it to (or simply highest optimization levels).

The Vee
  • 10,647
  • 5
  • 22
  • 50
  • 2
    Using `volatile` has its own cost. – Jarod42 Jul 24 '18 at 08:34
  • 5
    On *the platform you looked at*, they are identical – Caleth Jul 24 '18 at 08:34
  • @Caleth Would it be a non-conforming implementation if they left the non-volatiles unimplemented and relied on the `volatile` variant to get automatically called? – The Vee Jul 24 '18 at 08:36
  • @Jarod42 That sounds like it could be it. Do you have a little code to demonstrate this? – The Vee Jul 24 '18 at 08:38
  • 2
    `volatile` force to reload `this` each time its access to it. so 2 consecutive `load` would produce different asm depending of volatile or not. – Jarod42 Jul 24 '18 at 08:45
  • @Jarod42 But atomics already do that? – Passer By Jul 24 '18 at 08:53
  • @PasserBy: I meant `atom.load(); atom.load();` -> (pseudo asm) `read atom; call load; /*read atom;*/ call load;` The second `read atom;` is needed in case of `volatile`. – Jarod42 Jul 24 '18 at 08:57
  • 3
    @Jarod42 `this` has type `volatile atomic*`, not `atomic* volatile` and doesn't require a reload, if that's what you're getting at. – Passer By Jul 24 '18 at 09:04
  • 1
    @PasserBy: Indeed, it is not `this`, but `*this` which is `volatile`, so `_M_base` need a reload. – Jarod42 Jul 24 '18 at 09:09
  • "_I found all of them to be exact duplicates, for example,_" The posted code is **not** an example of "exact duplicates". – curiousguy Jul 26 '18 at 01:32
  • @curiousguy The function body is the same. Of course, now the members etc. are volatile so the generated assembly can differ. Can you elaborate what is the difference it makes? I'm still looking for an answer. – The Vee Jul 30 '18 at 07:57
  • @TheVee You don't show the declaration of called functions. Are they all volatile? – curiousguy Jul 30 '18 at 09:08
  • @curiousguy I can't find any difference in `__atomic_base` (which is the type of `_M_base`) either: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/libstdc++/api/a00739_source.html#l00389. The function called from there, `__atomic_load_n`, looks like a built-in, if that encloses the difference I don't know. In the spirit of my update, tests that I have tried showed none so far, but they are far from systematic. – The Vee Jul 30 '18 at 09:46

1 Answers1

1

Probably it all stems from what volatile is, for that see this answer. As the use-cases are quite slim compare to usual application development, that is why nobody usually cares. I will assume that you do not have any practical scenario where you wondering if you should apply those volatile overloadings. Then I will try to come up with an example where you may need those (do not judge it to be too real).

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 guarantees atomicity and modification order consistency, no side-effects. volatile cannot be reordered with side-effects. Then here we are, in thread 2 you can get shareable equal 10, but status is still not SIGINT. However, if you set type qualifier to volatile of shareable that must be guaranteed. For that you will need the member methods be volatile-qualified.

Why would you do something like this at all? One case, I might think of, is you have some old code that is using old volatile-based stuff and you cannot modify it for one or another reason. Hard to imagine, but I guess that there might be a need to have some kind of guaranteed order between atomic and volatile inline assembly. The bottom line, IMHO, is that whenever it is possible you can use new atomic library instead of volatile objects, in the case there are some volatile objects, that you cannot rid of, and you want to use atomic objects, then you might need volatile qualifier for the atomic objects to have proper ordering guarantees, for that you would need overloading.

UPDATE

But if all I wanted was to have atomic types usable as both volatile and non-volatile, why not just implement the former?

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;
}

The same would be with load and other operations, because those are usually not trivial implementations that require copy operator and/or copy constructor (that also can volatile or non-volatile).

Yuki
  • 3,234
  • 2
  • 20
  • 30
  • Sorry, I think I understand the reasoning but not how that explains why both `volatile` and non-`volatile` are needed, with the same body. So the order guarantee would be restored if the `atomic` was also `volatile`, right? But if all I wanted was to have `atomic` types usable as both `volatile` and non-`volatile`, why not just implement the former? – The Vee Aug 01 '18 at 08:46
  • @TheVee Yes, `volatile atomic` gives you order guarantees . If I correctly understood your second question: `volatile` on members is a specifier to compiler similar to `const`, see [this](https://stackoverflow.com/a/4826755/2228338), you have to duplicate implementation due to `atomic` being a class template, you do not want to force all the users force to mark their methods with `volatile` for `MyClass` that is a type of an object member in `atomic`. – Yuki Aug 01 '18 at 10:05
  • I think the last sentence is exactly what I'm looking for, but I'm still not completely sure. What methods would `MyClass` need to declare as `volatile` if this was not the case? The generic `atomic` provides just a couple operations, effectively just `load`, `store`, and `exchange` plus some porcelain, so perhaps (only) `operator=`? – The Vee Aug 01 '18 at 14:52