std::locale - behind the scenes

std::locale ist letztlich "nur" ein Container, der Facettenobjekte hält. Dabei soll er allerdings abgeleitete Facetten auf ihren Ursprung zurückführen können. Wie funktioniert das?

Video

Quelltext

#include <locale>
#include <map>
#include <memory>
#include <iostream>
#include <stdexcept>

class facet
{
public:
    virtual ~facet() {}
};

class locale
{
    
public:
    
    class id
    {
    public:
        
        id()
        {
        }
        
        id (id const &) = delete;
        id &operator=(id const &) = delete;
    };

    locale()
    {
    }
    
    template <typename facet_t> locale(locale const &src, facet_t *f)
        : mFacets(src.mFacets)
    {
        mFacets[&facet_t::id] = std::shared_ptr<facet>(f);
    }

protected:
    
    template <typename facet_t> friend facet_t const &use_facet(locale const &loc);
    
    facet const &getFacet(id const &i) const
    {
        auto elem = mFacets.find(&i);
        if (elem == mFacets.end()) {
            throw std::runtime_error("facet ist nicht Bestandteil der locale");
        }
        return *elem->second;
    }
    
private:
    std::map<id const *, std::shared_ptr<facet>> mFacets;
};

template <typename facet_t> facet_t const &use_facet(locale const &loc)
{
    return dynamic_cast<facet_t const &>(loc.getFacet(facet_t::id));
}

struct example_base : facet
{
    static locale::id id;
};

locale::id example_base::id;

struct example_derived : example_base
{
};

struct unrelated : facet
{
    static locale::id id;
};

locale::id unrelated::id;

int main()
{
    locale empty;
    auto u = new unrelated;
    
    locale global { empty, u };
    
    auto eb = new example_base;
    auto ed = new example_derived;
    
    locale l { global, eb };
    std::cout << "eb: " << eb << ", ed: " << ed << ", aus locale: "
              << &use_facet<example_base>(l) << ", unrelated: " << u << ", aus locale: " 
              << &use_facet<unrelated>(l) << std::endl;

    locale l2 { l, ed };
    std::cout << "eb: " << eb << ", ed: " << ed << ", aus locale: " 
              << &use_facet<example_base>(l2) << ", unrelated: " << u << ", aus locale: " 
              << &use_facet<unrelated>(l2) << std::endl;
}

Erklärung

Herzstück der Lösung ist das in jeder Basisfacette enthaltene id-Feld. Da jede abgeleitete Facette sich dieses Feld mit ihrer Basisfacette teilt (egal über wieviele Stufen abgeleitet wurde), dient dieses als Marker für ein bestimmtes, durch die Basisfacette repräsentiertes Konzept. Im Beispiel oben haben example_base und unrelated jeweils ihr eigenes id-Feld, während sich example_derived ihres mit example_base teilt. Aus Sicht von std::locale müssen also die Objekte der jeweiligen Klassen das gleiche repräsentieren und dürfen im Container daher nur einmal abgelegt werden. 

Der eigentliche Speicher für die Facettenobjekte ist die std::map in Zeile 54. Hier wird von einem Pointer auf ein id-Feld auf einen std::shared_ptr auf das Facettenobjekt abgebildet. Da für jede Basisfacette und all ihre abgeleiteten Facetten genau ein id-Objekt besteht und dieses damit genau an einer Stelle im Speicher liegt, ist der Pointer eine eindeutige Identifikation der Basisfacette. Egal, welcher Typ aus dem Ableitungsbaum instanziiert wird, das id-Feld wird immer den gleichen Pointer aufweisen. Daher kann der Pointer einfach als Index verwendet werden. 

Beim Einfügen einer neuen Facette (über den Konstruktor in Zeile 34ff) wird einfach ein Raw-Pointer auf das Facettenobjekt übernommen, in einen std::shared_ptr verpackt und unter dem passenden Index in die std::map eingefügt. Das entspricht der Spezifikation von std::locale: die Locale wird Eigentümer des Facettenobjektes. Wird die Locale kopiert, so teilt sie sich ihre Facettenobjekte mit ihrer Kopie. Wenn die letzte Locale mit einer bestimmten Facette gelöscht wird, wird auch die Facette selbst vernichtet. Genau dieses Verhalten wird durch std::shared_ptr erreicht. Es werden immer nur die Pointer kopiert und das eigentliche Objekt wird per Referenzzähler verwaltet und ggf. gelöscht.

Wird eine bestimmte Facette gesucht, so wird der Lookup wieder über den Pointer auf das id-Feld durchgeführt. Zu diesem Zweck ermittelt die protected-Funktion getFacet() in Zeile 46 diesen Pointer und liefert ggf. das dazu passende Objekt zurück. Das id-Feld wird von use_facet geliefert, einer Templatefunktion, die das aus ihrem Typparameter ermitteln kann. Hier kommt zum Tragen, dass ein static-Feld an keinem Objekt hängt (deswegen teilen sich ja alle Objekte das gleiche Feld), sondern dem Typ zugeordnet ist. Damit kann man auch nur mit Kenntnis des Typs auf das passende id-Feld zugreifen. 

In der main-Funktion werden die Eigenschaften demonstriert, indem erst eine locale erzeugt wird, die ein example_base-Objekt enthält (Zeile 90). Diese wird dann kopiert und das example_derived-Objekt aufgenommen. Laut Spezifikation sollte es das bereits vorhandene example_base ersetzen, da beide das gleiche id-Feld haben und damit das gleiche Konzept repräsentieren. Da unrelated-Objekt muss dabei unangetastet bleiben. Wie aus den Programmausgaben sichtbar wird, funktioniert das auch genau so.