1

My codebase contains this very simple templated function that gets called from a lot of different places:

// Returns a read-only reference to a default-constructed
// object of the specified type
template <typename T> const T & GetDefaultObjectForType()
{
   static const T _defaultObject{};
   return _defaultObject;
}

This function is useful in templated code, since it allows the templated code to access a default-constructed object of the type it is templated on, without having to construct one for that purpose.

It works nicely, but the problem I ran into today is that helgrind is telling me there is a race condition when I have multiple threads that call this function; that is because g++'s underlying implementation of the function apparently looks something more like this (pseudocode):

template <typename T> const T & GetDefaultObjectForType()
{
   static char _defaultObject[sizeof(T)];
   static std::atomic<bool> _isConstructed = false;
   if (_isConstructed == false)
   {
      _isConstructed = true;
      new (_defaultObject) T();  // demand-construct the object!
   }
   return reinterpret_cast<const T &>(_defaultObject);
}

... so because of the above, helgrind tells me that thread A's call to GetDefaultObjectForType() has written to the object (i.e. to demand-construct it) while thread B's code immediately after its call to GetDefaultObjectForType() has read from the object, and therefore there is a race condition.

I'm not sure if this is a real, will-bite-me-in-the-butt-someday race condition or just a false-positive caused by helgrind being too sensitive, but in either case, my question is the same: Is there some technique I can use to force the construction of the object to happen at (or near) the top-of-main, i.e. before any threads have been spawned, so that the singleton-object can be truly read-only as far as all the threads that call this function are concerned?

Obviously I could manually call GetDefaultObjectForType<EveryTypeIUse>() at the top of main(), but that would be a real maintenance hassle since I use so many different types of object that I'd be likely to forget to include some of the types (and it would force my main.cpp to have to #include a lot of header files that I'd prefer it not have to include).

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • 2
    Does this answer your question? [Is local static variable initialization thread-safe in C++11?](https://stackoverflow.com/questions/8102125/is-local-static-variable-initialization-thread-safe-in-c11) – dewaffled Aug 13 '23 at 01:45
  • @dewaffled possibly, but if so it raises the question of why helgrind reports a race there. I wonder if my version of g++ is buggy, or if helgrind is reporting a false positive, or ? – Jeremy Friesner Aug 13 '23 at 02:10
  • You can look at how your function is compiled: https://godbolt.org/z/5T8vG8feP It appears that there is thread syncronization here, done by __cxa_guard_acquire – Дмитрий Воронецкий Aug 13 '23 at 05:25
  • Thread-safe static initialization is *required* by the C++ standard in this specific use-case and this is a very common use (e.g. Meyers' singleton), so it would be very surprising if it is a GCC bug. Maybe Helgrind can report false positif. This is not so rare with such tools (especially for static variable actually). Or maybe there is a bug in Helgrind so it does not consider this case. – Jérôme Richard Aug 13 '23 at 11:52
  • I will require a citation of the pseudocode. You are asserting a bug in gcc without any evidence I can see, other than helgrind complaining. It is far more likely you do have a race condition in your actual code and helgrind has no bug, or helgrind has a bug. – Yakk - Adam Nevraumont Aug 14 '23 at 18:32
  • @Yakk-AdamNevraumont "I wonder if" is hardly an assertion, but you're right, a way to reproduce the fault would useful. I've posted a .zip file containing a trivial example program, a Makefile to compile and run it, and the output I get from `helgrind` and `objdump` when I reproduce the fault, here: https://public.msli.com/lcs/jaf/helgrind_test.zip (FWIW I'm compiling with g++ 10.3.0 on an Ubuntu Linux VM, and testing with both valgrind 3.16.1 and the latest valgrind from git, aka valgrind-3.22.0-GIT) – Jeremy Friesner Aug 14 '23 at 21:29
  • @JeremyFriesner I am asking about "that is because g++'s underlying implementation of the function apparently looks something more like this" - that isn't "I wonder if", but an assertion. What basis do you have for the produced code "looking like" that? – Yakk - Adam Nevraumont Aug 14 '23 at 22:59
  • @Yakk-AdamNevraumont my basis for thinking that the static variable is initialized on the first call to the function is the report supplied to me by helgrind indicating that `GetDefaultObjectForType()` does a write to the `_defaultObject`, the first time it is called, and that in doing so it causes a race-condition. – Jeremy Friesner Aug 14 '23 at 23:14
  • @JeremyFriesner "and that in doing so it causes a race-condition" - and your belief it causes a race-condition is based off a single tool generating a diagnostic message? I'm trying to figure out if you have an actual basis for your belief in that pseudo code other than helgrind. – Yakk - Adam Nevraumont Aug 15 '23 at 14:14
  • I'm really not interested in whatever this pointless argument is about. I'm interested in understanding why I'm getting the report from helgrind, and if there is any way to force a static variable to be initialized at process-start rather than the first time the function is called. If you can shed any light on those issues, I welcome it; OTOH if you're just here to sea-lion me over what you've decided I believe, then I'm going to ignore you. – Jeremy Friesner Aug 15 '23 at 19:03

0 Answers0