23 hours ago, Defend said:
Frob, I too haven't heard an argument that the namespace approach is worse (or not) than the class approach, but that's because it is practically impossible to find discussion on that particular question at all
That is because as others point out, it is syntactic sugar over the same issue.
The issue is mutable shared states. Mixing mutable shared state in any sufficiently large system is going to face issues with it. Two threads are going to modify it, or unrelated systems will fight over it, or similar. They introduce hidden coupling, introduce hidden dependencies, complicate or break tools like dependency injection, block extension, and cause many other issues.
The Singleton pattern (which means there can be only one) is that issue and more. In addition to shared mutable state it also creates unrealistic and improbable demands that a single instance is the only one that will ever be wanted, that no code will ever want to replace it, or extend the behavior, or replace the behavior, or provide either alternate or missing behavior, or many other conditions besides. It is well-covered as being "evil".
As for the shared mutable states in C and C++, there are a few that cannot be removed. They are holdovers from decades ago (probably long before you were born) when parallel processing was rare. There are the global streams (stdin, stdout, stderr) and their associated global locale; they have c++ class equivalents but they remain the three streams. These cannot be designed out of the language nor is there a good way to remove them universally, so they are here to stay.
Some shared mutable states remain because removing them would break too much code, but they have alternates for new code to use. There are strerror(), strtok(), and asctime() functions that all have static buffers internally. There are a few oldcharacter conversion functions between multibyte and wide characters that have shared state, such as wctomb(), mbtowc(), wcsrtomb(), and mbsrtowc(). There are some math functions like the gamma() family and rand() family that rely on internal state. All of these have alternate versions available that do not rely on shared mutable state.
And a few historically were shared but have been corrected, such as the errno codes from system libraries. In some cases there were rather extensive system library changes to support it, but the change was still made.
There are some on hardware as well. Floating point control is often fought-over between libraries. Setting the precision, denormalization options, error handling, and floating point exception resolution are commonly troublesome. Floating point rounding modes (even/nearest, up, down, and toward zero) can also lead to some 'gotcha' bugs.
23 hours ago, Defend said:
Thank you all though for confirming for me that I'm not just missing something obvious in C++.
This is common to all languages. You can mask it in various ways, including hiding in namespaces or wrapping on functions that modify hidden variables, but ultimately the underlying problem remains.
Even in functional languages, where there is tremendous effort made by the language designers to avoid stateful conditions in general, can still occasionally be stung by unintended shared mutable state.
As programs get larger and systems grow they will nearly always have some mutable shared states since there are times when the engineering effort required to avoid it is greater than the project is willing to bear, but that should be weighed as an intentional choice to implement the shared state, and mitigated through policies such as ensuring modification only happens at specific times, or by specific systems, or in specific manners.
These rules can be put in place in many ways, including having functions that modify a static variable that is kept out of visible scope (such as within an anonymous namespace or as a static variable within a file) but they remain an implementation of a shared mutable state.