Je me demandais s'il était possible de s'assurer qu'une fonction n'est appelée que pendant l'étape d'initialisation statique d'un programme?
Par exemple, disons que j'ai une classe singleton qui contient un objet std::map
et expose ses méthodes insert
et at
. Je voudrais m'assurer que la lecture des données à partir de celle-ci (la méthode at
) est thread-safe, ce qui, à ma connaissance, nécessite de s'assurer que rien ne modifie les données (c'est-à-dire en utilisant la méthode insert
.).
La carte est destinée à être remplie uniquement lors de l'initialisation statique, heure à laquelle je suppose qu'il n'y a qu'un seul thread. Existe-t-il un moyen de ne pas éviter les appels d'utilisateurs non guidés insert
une fois que main()
a commencé?
Exemple de 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
}
Inutile (?) De dire que le code actuel est bien plus complexe que ce simple exemple.
Edition après solution: Il semble que le meilleur moyen de rendre ceci aussi sûr que possible est de conserver une variable status
qui enregistre si le singleton est en mode 'insert' ou 'read' et agit en conséquence. Merci à tous pour leurs idées et suggestions!
Je suppose que vous souhaitez également utiliser la méthode 'at' pour configurer votre application . Pourquoi ne pas ajouter une méthode 'lock' et appeler cette simple fonction en tant que première fonction principale?
#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;
}
Si vous pouvez garantir que l'utilisateur ne lira pas la carte avant la main()
à l'étape d'initialisation, une solution consiste à construire une carte statique uniquement pour l'initialisation, puis à la déplacer vers le singleton lors de la construction du singleton.
Étant donné que la construction a lieu au premier appel de instance()
, vous pouvez être sûr que la carte est correctement initialisée.
Un autre appel à insert
n'aura alors aucun effet sur le singleton. Vous pouvez également ajouter un mutex pour protéger insert
afin d’éviter que UB ne soit en situation de concurrence.
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);
}
};
Tout comme Jürgen avec la méthode non Java mais la méthode c/c ++ de créer un singleton (un espace de noms).
Pourquoi faire comme ça:
this
pour accéder à l'état;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);
}
}
De plus, si le singleton doit être utilisé dans un code générique, une structure peut être utilisée pour cela:
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!!!
Demain, arrêtez de coder en Java :).