In C++, can I safely initialize an unordered_map with values from different files?

631 views Asked by At

Imagine code like this:

std::unordered_map<std::string, std::function<Foo *()>> FooFactory;
void registerFoo(std::string name, std::function<Foo *()> factory)
{
    FooFactory.emplace(name, factory);
}

If I now would write code like this in another file:

static bool Baz = [](){ registerFoo("baz", []() { return new BazFoo(); })}();

And yet another:

static bool Bar = [](){ registerFoo("bar", []() { return new BarFoo(); })}();

In this case registerFoo is called as the program initializes, but FooFactory is then zeroed out, so the registered functions disappear.

Is there a way to make this work in a safe, compiler-independent way (for c++14)?

4

There are 4 answers

3
Barry On BEST ANSWER

You could stick the factory itself inside of a function:

std::unordered_map<std::string, std::function<Foo *()>>& getFactory() {
    static std::unordered_map<std::string, std::function<Foo *()>> FooFactory;
    return FooFactory;
}

Which your registration function can go through:

void registerFoo(std::string name, std::function<Foo *()> factory)
{
    getFactory().emplace(name, factory);
}

This should guarantee ordering.

3
user7860670 On

To avoid static initialization order fiasco you can wrap up factory access into function call constructing it during first call:

using
t_NameToFactoryMap = std::unordered_map<std::string, std::function<Foo *()>>

t_NameToFactoryMap * p_foo_factory{}; // initialized with nullptr before dynamic initialization starts

t_NameToFactoryMap &
getFooFactory(void)
{
    if(!p_foo_factory)
    {
        p_foo_factory = new t_NameToFactoryMap{};
    }
    return(*p_foo_factory);
}

void registerFoo(std::string name, std::function<Foo *()> factory)
{
    getFooFactory().emplace(name, factory);
}

The drawback of such autoregistration methods is that they will create problems if you decide to utilize them in some static library project. Projects using this static library won't reference Baz or Bar unless they link this static library using some compiler-dependent flag, such as --whole-archive for gcc.

So a better solution would be to explicitly create factory below main and register all the required items without dealing with dynamic initialization at all.

1
Yakk - Adam Nevraumont On

First, you want some thread safety:

template<class T, class M=std::shared_timed_mutex> // shared_mutex in C++17
struct mutex_guarded {
  template<class F>
  auto write( F&& f )
  ->std::decay_t<std::result_of_t<F(T&)>> {
    auto l = lock();
    return std::forward<F>(f)(t);
  }
  template<class F>
  auto read( F&& f ) const
  ->std::decay_t<std::result_of_t<F(T const&)>> {
    auto l = lock();
    return std::forward<F>(f)(t);
  }
  mutex_guarded() {}
  template<class T0, class...Ts,
    std::enable_if_t<!std::is_same<std::decay_t<T0>, mutex_guarded>{},int> =0
  >
  mutex_guarded(T0&& t0, Ts&&...ts):
    t(std::forward<T0>(t0), std::forward<Ts>(ts)...)
  {}
  mutex_guarded( mutex_guarded const& o ):
    t(o.copy_from())
  {}
  mutex_guarded( mutex_guarded && o ):
    t(o.move_from())
  {}
  mutex_guarded& operator=(mutex_guarded const&)=delete;
  mutex_guarded& operator=(mutex_guarded &&)=delete;
  mutex_guarded& operator=(T const& t) {
    write([&t](T& dest){dest=t;});
    return *this;
  }
  mutex_guarded& operator=(T&& t) {
    write([&t](T& dest){dest=std::move(t);});
    return *this;
  }
private:
  T copy_from() const& { return read( [](T const& t){ return t; } ); }
  T copy_from() && { return move_from(); }
  T move_from() { return write( [](T& t){ return std::move(t); } ); }

  std::unique_lock<M> lock() const {
    return std::unique_lock<M>(m);
  }
  std::shared_lock<M> lock() {
    return std::shared_lock<M>(m);
  }
   M m; // mutex
   T t;
};

which lets us have a:

using foo_factory = std::function<std::unique_ptr<Foo>()>;
using foo_factories = std::unordered_map<std::string, foo_factory>;
mutex_guarded<foo_factories>& get_foo_factories() {
  static mutex_guarded<foo_factories> map;
  return map;
}

which has thread-safe initialization, then

void registerFoo(std::string name, std::function<Foo *()> factory)
{
  get_foo_factories().write([](auto& f){f.emplace(name, factory);});
}

is thread safe and guarantees initialization of the factories early enough.

At shutdown, the timing of the destruction of the factories is both beyond your control (reverse order of construction) and may be too early.

mutex_guarded<foo_factories>*& get_foo_factories() {
  static auto* map = new mutex_guarded<foo_factories>;
  return map;
}
void registerFoo(std::string name, std::function<Foo *()> factory)
{
  get_foo_factories()->write([](auto& f){f.emplace(name, factory);});
}
void end_foo_factories() {
  auto*& ptr = get_foo_factories();
  delete ptr; ptr = nullptr;
}

this will place it on the heap where it is going to live a bit longer. Note this also leaks both the factories and the map; manual destruction "late enough" can be added. Note that this destruction is not thread safe, nor can it cheaply be made thread safe; it should occur after all threads have been cleaned up.

0
Steve Rodeen On

Although using a global context like this is not encouraged, here are some more items you should consider in addition to @Barry's answer:

  1. You should guard the emplace with a mutex (in case multiple threads try to add to the unordered_map)
  2. Optionally return the success argument of emplace (accessed by second).
  3. Forward your arguments to ensure perfect forwarding:
bool registerFoo(std::string &&name, std::function<Foo *()> &&factory)
{
    static std::mutex register_mutex;
    std::lock_guard<std::mutex> lock(register_mutex);

    return getFactory().emplace(
            std::forward<std::string>(name),
            std::forward<std::std::function<Foo *()>>(factory)
        ).second;
}

and then:

static bool Baz = [](){ return registerFoo("baz", []() { return new BazFoo(); })}();

Don't forget to make a facility to delete all these free function pointers when you don't need them anymore.