2017-08-30 7 views
3

Imagine Code wie folgt:Kann ich in C++ eine unordered_map mit Werten aus verschiedenen Dateien sicher initialisieren?

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

Wenn ich würde jetzt Code wie folgt in einer anderen Datei schreiben:

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

Und noch eine:

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

In diesem Fall registerFoo heißt Wenn das Programm initialisiert wird, wird FooFactory jedoch auf Null gesetzt, so dass die registrierten Funktionen verschwinden.

Gibt es eine Möglichkeit, dies auf eine sichere, Compiler-unabhängige Weise (für C++ 14) zu machen?

Antwort

10

Sie konnten die Fabrik stecken sich innerhalb einer Funktion:

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

Welche Ihrer Registrierungsfunktion durchlaufen kann:

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

Diese Reihenfolge garantieren sollte.

+0

Das ist was ich tue. Ich mache auch manchmal einen 'boost ::' oder 'std :: optional &', der standardmäßig mit einem 'blah' konstruiert wird, weil das mir erlaubt, solche Singletons in der von mir gewählten Reihenfolge zu zerstören, anstatt in Compiler- bestimmte pro-Laufzeit umgekehrte Reihenfolge der Konstruktion. – Yakk

+0

Ich beachte, dass die obige Lösung von VTT ein dynamisch zugewiesenes Objekt verwendet. Hat das irgendeinen Vorteil? – Nuoji

+1

Die typische Rechtfertigung für die Verwendung von dynamischer Zuordnung oder ähnlichem Hacking besteht darin, das Objekt während der Zerstörung globaler Objekte verfügbar zu machen. Kann oder kann kein tatsächliches Problem sein, abhängig vom Anwendungsfall. –

0

Um statisches Initialisierungsreihenfolge Fiasko zu vermeiden können Sie Fabrik Zugang in Funktionsaufruf während ersten Aufruf Konstruktion einpacken:

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); 
} 

Der Nachteil einer solchen automatischen Registrierung Methoden ist, dass sie Probleme verursachen, wenn Sie sich entscheiden, sie zu nutzen, in einem statischen Bibliotheksprojekt. Projekte, die diese statische Bibliothek verwenden, verweisen nicht auf Baz oder Bar, es sei denn, sie verknüpfen diese statische Bibliothek mit einem Compiler-abhängigen Flag, z. B. --whole-archive für gcc.

Eine bessere Lösung wäre also, Factory unter main explizit zu erstellen und alle erforderlichen Elemente zu registrieren, ohne überhaupt mit der dynamischen Initialisierung zu arbeiten.

+1

Ich mag die Idee, aber ich mag die Implementierung nicht. Sie haben ein 'neues' ohne' delete' und obwohl es kein Problem sein sollte, ist es immer noch unansehnlich. Wenn Sie 'getFooFactory()' immer aufrufen müssen, können Sie die Map auch als statische Variable innerhalb der Funktion definieren, die den Zeiger "new" loswird und Ihre eigene Initialisierungsprüfung durchführen muss. – NathanOliver

+0

@NathanOliver Ich stimme zu, dass dies eine eher skizzenhafte Lösung ist, die die Autoregistrierung unabhängig von der Initialisierungsreihenfolge funktioniert. Der wichtige Teil ist jedoch, dass die Autoregistrierung immer noch nicht funktioniert, wenn sie innerhalb der statischen Bibliothek ohne kompilerspezifische Magie verwendet wird. – VTT

+0

Dies ist nicht threadsicher. Verwenden Sie eine magische Statik für die Threadsicherheit, auch wenn Sie einen Zeiger und eine dynamische Zuweisung verwenden. – Yakk

1

Obwohl ein globalen Kontext wie dies mit nicht gefördert wird, sind hier einige weitere Elemente, die Sie zusätzlich zu @ Barrys Antwort in Betracht ziehen sollten:

  1. Sie sollten bewachen die emplace mit einem mutex (bei mehreren Threads versuchen zum unordered_map hinzufügen)
  2. Geben Sie optional das Erfolgsargument von emplace zurück (Zugriff von second).
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; 
} 

und dann:

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

Vergessen Sie nicht, eine Einrichtung zu machen all diese freien Funktionszeiger zu löschen, wenn Sie nicht tun

  • Vorwärts Ihre Argumente perfekte Weiterleitung, um sicherzustellen, brauche sie mehr.

  • 1

    Erstens wollen Sie einige Thread-Sicherheit:

    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; 
    }; 
    

    , die uns ein lets have:

    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; 
    } 
    

    die Thread-sichere Initialisierung hat, dann

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

    ist Thread-sicher und garantiert früh genug die Initialisierung der Fabriken.

    Bei der Abschaltung ist das Timing der Zerstörung der Fabriken sowohl außerhalb Ihrer Kontrolle (umgekehrte Reihenfolge der Konstruktion) und möglicherweise zu früh.

    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; 
    } 
    

    Dies wird es auf den Haufen legen, wo es ein bisschen länger leben wird. Beachten Sie, dass dadurch auch die Fabriken und die Karte verloren gehen. manuelle Zerstörung "spät genug" kann hinzugefügt werden. Beachten Sie, dass diese Zerstörung nicht Thread-sicher ist, noch kann es billig threadsicher gemacht werden; Es sollte auftreten, nachdem alle Threads bereinigt wurden.

    +1

    Wenn die registerFunctions nur während der statischen Initialisierung aufgerufen werden, ist es threadsicher und wird standardmäßig garantiert. – balki

    Verwandte Themen