7

I was wondering if it is possible to ensure that a function is only called during the static initialization step of a program?

As an example, say that I have some singleton class that contains a std::map object and exposes the insert and at methods of it. I would like to ensure that reading data from it (the at method) is thread-safe which, to my understanding, requires ensuring that nothing is modifying the data (i.e. using the insert method).

The map is intended to be filled only during static initialization, at which time I'm assuming there is only one thread. Is there any way that I can ensure no misguided user calls insert once main() has begun?


Example code

#include <map>
#include <string>

class Singleton {
  private:
    std::map<std::string, std::string> m_map;
  public:
    static Singleton& instance() {
      static Singleton theSingleton;
      return theSingleton;
    }
    static bool insert(const std::string& key, const std::string& value) {
      return instance().m_map.insert(std::make_pair(key, value) ).second;
    }
    static std::string at(const std::string& key) {
      return instance().m_map.at(key);
    }
};

static bool inserted = Singleton::insert("Hello", "World"); // fine

bool addItem(const std::string& key, const std::string& value) {
  return Singleton::insert(key, value); // not OK
}

Needless(?) to say the actual code is a good deal more complex than this simple example.


Edit after solution: It looks like the best way to make this as safe as possible is to maintain a status variable that records whether the singleton is in 'insert' or 'read' mode and act accordingly. Thanks to all for their ideas and suggestions!

extiam
  • 95
  • 6
  • how about making insert a private function that is called with the constructor and making the map const? – Adam Zahran Dec 19 '18 at 10:05
  • @AdamZahran Other translation units should, in principle, be allowed to add information to the map (so long as they do it before main). – extiam Dec 19 '18 at 10:07
  • If I understand the scenario correctly, the insert function shall be called from the outside. Otherwise, initialization of the `std::map m_map = {{"hello", "world"}};` would be enough anyway, and there was no need for `Singleton::insert`. – lubgr Dec 19 '18 at 10:08
  • 4
    That does not seem to be a very good design choice, as the map contents are a bit unpredictable in that case, e.g. if two distinct values are inserted with same key in two separate compilation units, which one will end up in map? – paler123 Dec 19 '18 at 10:13
  • @paler123 Yeah - there are definite risks with this approach and I have to have things in place to ensure that keys are always unique. However the number of different pieces of information to be stored in this map is fairly limited and in practice it should be possible to ensure that users use unique keys. – extiam Dec 19 '18 at 10:21
  • @AdamZahran unless I'm missing something that solution cannot work if the information is to be provided in separate translation units - the template specializations would not be visible to the compiler. – extiam Dec 19 '18 at 10:28
  • why wouldn't the template specializations be visible to the compiler? – Adam Zahran Dec 19 '18 at 10:33
  • By "ensure" do you mean that there should be compiler errors if `insert` is called elsewhere? – P.W Dec 19 '18 at 10:36
  • @AdamZahran Say I have two translation units. One (in static initialization) loads a value into the map, another (during the main function) tries to read this value. With a static map this behaviour works, with a template specialization the second translation unit would need to know at compile time that the specialization exists. – extiam Dec 19 '18 at 10:37
  • @P.W if it's possible to enforce a compiler error if `insert` is called outside of static initialization then that would be great - I don't know of any piece of the language that would allow that however. – extiam Dec 19 '18 at 10:39
  • 1
    Will you *read" the map before it's fully initialized? in other words, will the `at` method be called before main? – llllllllll Dec 19 '18 at 10:44
  • 1
    @liliscent `at` should not be called before main (or at least I cannot see any reason why it would be + I can make this a requirement if necessary). I _can_ guarantee that no `insert` call should depend on any `at` call. – extiam Dec 19 '18 at 10:46
  • maybe have your inserts in a header and include it in all TUs that needs them? Of course have your header guards in place to avoid multiple definitions. – Adam Zahran Dec 19 '18 at 10:50
  • The deal breaker here is that you need the key and value to be strings. Which is really hard to do at compile time. I don't even think it's possible, because strings are basically pointers, so even if you have a pointer for the key it has to be identical to that which was used for the template specialization, which is silly, we need the value of the string not its address. – Adam Zahran Dec 19 '18 at 10:54
  • @AdamZahran Yes, adding information to the map is impossible during compile time which is why I want to do it during static initialization. I want it to be possible for users to add their own information into the map _without_ editing some central file (and requiring that they take responsibility for not defining conflicting keys). – extiam Dec 19 '18 at 10:59
  • Is it an option to add a `status` member variable in the singleton that would be statically initialized to `INSERT_OK`, that would be set to `INSERT_NO` in main (before multithreading is initialized) and that would be tested in `insert`? BTW the read method could also test it and throw if called before initialization is finished. – Serge Ballesta Dec 19 '18 at 11:38
  • While this isn't portable, you may find GCC's [`init_priority` attribute](https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes) interesting. This can remove the need to call `lock` at the beginning of `main()` based on Jürgen's answer. Instead, you could have an static initializer with high priority value (which ironically means that this initializer will run later than others). Caveat: the documentation is IMO ambiguous over whether this will work across translation units. – Arne Vogel Dec 19 '18 at 12:48
  • A *truly* misguided user will create a thread before `main` and defeat the whole exercise. – n. 'pronouns' m. Dec 19 '18 at 13:07

3 Answers3

4

I guess you also want to use the 'at' method in setting up your application. Why not add a 'lock' method and call that simple as first function in the main?

#include <map>
#include <string>

class Singleton {
private:
    std::map<std::string, std::string> m_map;
    bool m_locked;
    Singleton() : m_locked(false) { }

public:
    static Singleton& instance() {
        static Singleton theSingleton;
        return theSingleton;
    }

    static void lock() {
        instance().m_locked = true;
    }

    static bool insert(const std::string& key, const std::string& value) {
        if (instance().m_locked) { return false; }
        return instance().m_map.insert(std::make_pair(key, value)).second;
    }
    static std::string at(const std::string& key) {
        return instance().m_map.at(key);
    }
};

static bool inserted = Singleton::insert("Hello", "World"); // fine

bool addItem(const std::string& key, const std::string& value) {
    return Singleton::insert(key, value); // not OK
}

int main(int argc, char** argv)
{
    Singleton::lock();
    Singleton::insert("Hello2", "World2"); // fails
    return 0;
}
Jürgen
  • 76
  • 3
2

If you can guarantee that user won't read the map before main() in the initialization stage, one solution is to construct a static map only for initialization, then move it to the singleton when the singleton is being constructed.

Since the construction happens the first time instance() is called, you can be sure that map is correctly initialized.

Then other call to insert won't have effect on the singleton. You can also add mutex to protect insert to avoid UB from race condition.

class Singleton {
  private:
    std::map<std::string, std::string> m_map;
    static auto& init_map() {
        static std::map<std::string, std::string> m;
        return m;
    }
    Singleton() {
        m_map = std::move(init_map());
        init_map().clear();
    }
  public:
    static Singleton& instance() {
      static Singleton theSingleton;
      return theSingleton;
    }
    static bool insert(const std::string& key, const std::string& value) {
      // you can also add mutex to protect here,
      // because calling insert from different threads without
      // protection will screw up its internal state, even if
      // the init_map becomes useless after main
      return init_map().insert(std::make_pair(key, value) ).second;
    }
    static std::string at(const std::string& key) {
      return instance().m_map.at(key);
    }
};
llllllllll
  • 15,080
  • 4
  • 23
  • 46
  • Can a race condition occur during static initialization (i.e. can there be multiple threads at this point)? – extiam Dec 19 '18 at 12:07
  • @extiam From how I read the standard, it is legal to create another thread prior to the execution of `main()` (i.e. from within a static initializer). If anyone were to do it, I'd question the wisdom of the design though. – Arne Vogel Dec 19 '18 at 12:41
  • It's not too expensive to protect against, unwise as it might be. I'm not anticipating a huge number of insert calls so adding a mutex isn't painful. – extiam Dec 19 '18 at 12:48
  • @Oliv That's true. Put it in another function as static local variable, the rest is the same. – llllllllll Dec 19 '18 at 13:28
1

Just like Jürgen with the non-Java way but c/c++ way of creating a singleton (ie a namespace).

Why doing this way:

  • it is much more efficient and easier to optimize at link time because there is no need to dereference this to access the state;
  • there is no need to maintain code to ensure uniqueness: uniqueness is ensured by the linker.

singleton.hpp

namespace singleton{
  void lock();
  bool instert(const std::string& key, const std::string& value);
  std::string at(const std::string& key)
  }

singleton.cpp

namespace singleton{
  namespace{
    inline decltype(auto) 
    get_map(){
      static std::map<std::string, std::string> m_map;
      return m_map;
      }
    bool m_locked=false; //OK guarenteed to happen before any dynamic initialization [basic.start.static]
    }

  void lock() {
    m_locked = true;
    }

  bool insert(const std::string& key, const std::string& value) {
    if (m_locked) { return false; }
    return get_map().insert(std::make_pair(key, value)).second;
    }

  std::string at(const std::string& key) {
    return get_map().at(key);
    }
  }

Also if the singleton must be used in a generic code a struct can be used for that:

struct singleton_type{
  static void lock() {singleton::lock();}
  static auto insert(const std::string& key, const std::string& value) {
      return singleton::insert(key,value);
      }
  static auto at(const std::string& key) {
      return singleton::at(key,value);
      }
  };
auto x = singleton_type{};
auto y = singleton_type{}; 
// x and y refers to the same and unique object file!!!

Tomorrow, stop coding in java :).

Oliv
  • 16,492
  • 1
  • 24
  • 63