Honestly the guard overhead is a non-issue in practice — it's one atomic check after first init. The real problem with the static data member approach is
initialization order across translation units. If singleton A touches singleton B during startup you get fun segfaults that only show up in release builds with a
different link order.
I ended up using std::call_once for those cases. More boilerplate but at least you're not debugging init order at 2am.