1

Let's say I have this application:

#include <atomic>
#include <thread>
#include <iostream>
#include <chrono>

void do_something(const std::atomic<bool>& stop) {
    while (!stop) {
        std::cout << "Doing stuff..." << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {
    std::atomic<bool> stop { false };

    std::thread wait([&stop] { 
        std::this_thread::sleep_for(std::chrono::seconds(10));
        stop = true;
    });

    do_something(stop);

    wait.join();
}

This works as expected. The main thread loops until the wait thread sets stop. As far as I know, there's no real issues with this application since it's using an std::atomic<bool> for synchronization between the threads.

However, I can break the application by changing the signature of do_something to this:

void do_something(const bool& stop);

This still compiles without any warnings, but the loop in do_something never exits. Conceptually, I understand why this happens. do_something is accepting a const reference to a non-atomic value, so it would be reasonable to optimize the function to something like:

void do_something(const bool& stop) {
    if (!stop) {
        while(true) {
            std::cout << "Doing stuff..." << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
}

What's confusing to me is why the modified application compiles at all. I wouldn't expect to be able to pass an std::atomic value into a function expecting a const reference to a non-atomic type. I haven't looked at the standard, but I don't see anything on cppreference that suggests this conversion is allowed.

Am I misunderstanding something here? Is this just some quirk of std::atomic that's not obvious to me from the documentation?

Matt K
  • 598
  • 5
  • 19
  • Is it really the case that the compiler is allowed to optimise here? In a multithreaded environment, the stop value can be changed even if it is read by a const reference in the meantime, can it not? https://onlinegdb.com/menLSBacx – SoulfreezerXP Jul 14 '22 at 17:35

1 Answers1

3

This is because there is an implicit conversion when you call do_something(const bool &stop) while passing an std::atomic<bool>

It translates to:

do_something(static_cast<bool>(stop.operator bool()));

As you can see here : https://en.cppreference.com/w/cpp/atomic/atomic/operator_T

You actually tell the compiler to load the value once, right at the time of the call.

Victor
  • 486
  • 3
  • 8
  • Interesting. I saw that operator but couldn't really make sense of it. So in my example, it's not even a case of `do_something` not seeing an update to the reference. It's just getting a reference to an entirely new copy of the value? – Matt K Apr 20 '22 at 16:41
  • 1
    take a look at your code C++ insights : https://cppinsights.io/s/fa1ca15e – Victor Apr 20 '22 at 16:42
  • you tell the compiler the value won't change by specifying the `const` keyword, so the compiler assumes the value won't ever change. Actually, if you remove the `const` keyword, it won't even compile, because it makes no sense. – Victor Apr 20 '22 at 17:02
  • In case someone stumbles across this and doesn't follow (like I didn't): It makes no sense without `const` because `operator bool()` is providing an rvalue as the argument to `do_something`. You can't pass an rvalue to a function by non-const reference, but you can by const reference. See https://stackoverflow.com/questions/36102728/why-is-it-allowed-to-pass-r-values-by-const-reference-but-not-by-normal-referenc – Matt K Apr 20 '22 at 17:14