std::async(std::launch::async, fun, nullptr);
Doesn't do anything with the returned std::future, leaving it to be destroyed. That's a problem because std::future's destructor may block and wait for the thread to finish.
The solution is to hold on to the std::future for a while and let it be destroyed after you're done with everything else.
auto locallyScopedVariable = std::async(std::launch::async, fun, nullptr);
locallyScopedVariable will go out of scope at the end of main and then block until it completes.
Note that this still might not do quite what you want. The main thread could immediately yield the processor to the new thread and allow the new thread to run to completion before control is returned. The code can be corrected and still result in the output of the incorrect version.