samedi 12 août 2023

Templated read-only singleton function vs thread-safety

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).

Aucun commentaire:

Enregistrer un commentaire