Was ist der Unterschied zwischen einem Merkmal und einer Richtlinie?

Was ist der Unterschied zwischen einem Merkmal und einer Richtlinie?

Richtlinien

Richtlinien sind Klassen (oder Klassenvorlagen) zum Einfügen von Verhalten in eine Elternklasse, typischerweise durch Vererbung. Durch die Zerlegung einer übergeordneten Schnittstelle in orthogonale (unabhängige) Dimensionen bilden Richtlinienklassen die Bausteine ​​komplexerer Schnittstellen. Ein häufig anzutreffendes Muster besteht darin, Richtlinien als benutzerdefinierbare Vorlagenparameter (oder Vorlagenvorlagenparameter) mit einem von der Bibliothek bereitgestellten Standard bereitzustellen. Ein Beispiel aus der Standardbibliothek sind die Allocators, die Richtlinienvorlagenparameter aller STL-Container sind

template<class T, class Allocator = std::allocator<T>> class vector;

Hier der Allocator Der Template-Parameter (der selbst auch ein Klassen-Template ist!) fügt die Speicherzuweisungs- und Freigaberichtlinie in die übergeordnete Klasse std::vector ein . Wenn der Benutzer keine Zuweisung angibt, wird standardmäßig std::allocator<T> verwendet verwendet wird.

Wie es für vorlagenbasierten Polymorphismus typisch ist, sind die Schnittstellenanforderungen an Richtlinienklassen implizit und semantisch (basierend auf gültigen Ausdrücken) und nicht explizit und syntaktisch (basierend auf der Definition virtueller Elementfunktionen).

Beachten Sie, dass die neueren ungeordneten assoziativen Container mehr als eine Richtlinie haben. Zusätzlich zu den üblichen Allocator Template-Parameter, nehmen sie auch einen Hash Richtlinie, die standardmäßig std::hash<Key> ist Funktionsobjekt. Dadurch können Benutzer von ungeordneten Containern diese entlang mehrerer orthogonaler Dimensionen konfigurieren (Speicherzuweisung und Hashing).

Eigenschaften

Merkmale sind Klassenvorlagen zum Extrahieren von Eigenschaften von einem generischen Typ. Es gibt zwei Arten von Merkmalen:einwertige Merkmale und mehrwertige Merkmale. Beispiele für einwertige Merkmale sind die aus dem Header <type_traits>

template< class T >
struct is_integral
{
    static const bool value /* = true if T is integral, false otherwise */;
    typedef std::integral_constant<bool, value> type;
};

Einwertige Merkmale werden oft in der Template-Metaprogrammierung verwendet und SFINAE-Tricks zum Überladen einer Funktionsvorlage basierend auf einer Typbedingung.

Beispiele für mehrwertige Traits sind iterator_traits und allocator_traits aus den Headern <iterator> und <memory> , beziehungsweise. Da Merkmale Klassenvorlagen sind, können sie spezialisiert werden. Nachfolgend ein Beispiel für die Spezialisierung von iterator_traits für T*

template<T>
struct iterator_traits<T*>
{
    using difference_type   = std::ptrdiff_t;
    using value_type        = T;
    using pointer           = T*;
    using reference         = T&;
    using iterator_category = std::random_access_iterator_tag;
};

Der Ausdruck std::iterator_traits<T>::value_type ermöglicht es, generischen Code für vollwertige Iteratorklassen auch für Rohzeiger nutzbar zu machen (da Rohzeiger kein Mitglied value_type haben ).

Interaktion zwischen Richtlinien und Merkmalen

Wenn Sie Ihre eigenen generischen Bibliotheken schreiben, ist es wichtig, darüber nachzudenken, wie Benutzer Ihre eigenen Klassenvorlagen spezialisieren können. Allerdings muss man aufpassen, dass Nutzer nicht der One-Definition-Rule zum Opfer fallen durch die Verwendung von Spezialisierungen von Merkmalen, um Verhalten zu injizieren, anstatt es zu extrahieren. Um diesen alten Beitrag von Andrei Alexandrescu zu paraphrasieren

Der C++11 std::allocator_traits vermeidet diese Fallstricke, indem er erzwingt, dass alle STL-Container Eigenschaften nur aus ihrem Allocator extrahieren können Richtlinien durch std::allocator_traits<Allocator> . Wenn Benutzer einige der erforderlichen Richtlinienmitglieder nicht angeben oder vergessen, diese anzugeben, kann die Traits-Klasse eingreifen und Standardwerte für diese fehlenden Mitglieder bereitstellen. Weil allocator_traits selbst kann nicht spezialisiert werden, Benutzer müssen immer eine vollständig definierte Zuweisungsrichtlinie übergeben, um die Speicherzuweisung ihrer Container anzupassen, und es können keine stillen ODR-Verletzungen auftreten.

Beachten Sie, dass man als Bibliotheksautor Traits-Klassen-Templates immer noch spezialisieren kann (wie es die STL in iterator_traits<T*> tut ), aber es empfiehlt sich, alle benutzerdefinierten Spezialisierungen durch Richtlinienklassen in mehrwertige Merkmale zu übergeben, die das spezialisierte Verhalten extrahieren können (wie es die STL in allocator_traits<A> tut ).

AKTUALISIEREN :Die ODR-Probleme benutzerdefinierter Spezialisierungen von Merkmalsklassen treten hauptsächlich auf, wenn Merkmale als globale Klassenvorlagen verwendet werden und Sie können nicht garantieren, dass allen zukünftigen Benutzern alle anderen benutzerdefinierten Spezialisierungen angezeigt werden. Richtlinien sind lokale Vorlagenparameter und enthalten alle relevanten Definitionen, sodass sie ohne Eingriff in anderen Code benutzerdefiniert werden können. Lokale Vorlagenparameter, die nur Typ und Konstanten enthalten – aber keine Verhaltensfunktionen – könnten immer noch als „Eigenschaften“ bezeichnet werden, aber sie wären für anderen Code wie std::iterator_traits nicht sichtbar und std::allocator_traits .


Ich denke, Sie finden die bestmögliche Antwort auf Ihre Frage in diesem Buch von Andrei Alexandrescu . Ich versuche hier nur einen kurzen Überblick zu geben. Hoffentlich hilft es.

Eine Eigenschaftsklasse ist eine Klasse, die normalerweise als Metafunktion gedacht ist, die Typen anderen Typen oder konstanten Werten zuordnet, um eine Charakterisierung dieser Typen bereitzustellen. Mit anderen Worten, es ist eine Möglichkeit, Eigenschaften von Typen zu modellieren . Der Mechanismus nutzt normalerweise Templates und Template-Spezialisierung, um die Assoziation zu definieren:

template<typename T>
struct my_trait
{
    typedef T& reference_type;
    static const bool isReference = false;
    // ... (possibly more properties here)
};

template<>
struct my_trait<T&>
{
    typedef T& reference_type;
    static const bool isReference = true;
    // ... (possibly more properties here)
};

Die Trait-Metafunktion my_trait<> oben verknüpft den Referenztyp T& und den konstanten booleschen Wert false für alle Typen T die nicht sind selbst Referenzen; andererseits ordnet es den Referenztyp T& zu und den konstanten booleschen Wert true für alle Typen T das sind Referenzen.

Also zum Beispiel:

int  -> reference_type = int&
        isReference = false

int& -> reference_type = int&
        isReference = true

Im Code könnten wir das Obige wie folgt behaupten (alle vier Zeilen unten werden kompiliert, was bedeutet, dass die im ersten Argument ausgedrückte Bedingung für static_assert() ist zufrieden):

static_assert(!(my_trait<int>::isReference), "Error!");
static_assert(  my_trait<int&>::isReference, "Error!");
static_assert(
    std::is_same<typename my_trait<int>::reference_type, int&>::value, 
    "Error!"
     );
static_assert(
    std::is_same<typename my_trait<int&>::reference_type, int&>::value, 
    "Err!"
    );

Hier konnte man sehen, dass ich den Standard std::is_same<> verwendet habe -Vorlage, die selbst eine Metafunktion ist, die zwei akzeptiert , anstatt eines, geben Sie argument ein. Hier kann es beliebig kompliziert werden.

Obwohl std::is_same<> ist Teil des type_traits -Header, einige betrachten eine Klassenvorlage nur dann als Typmerkmalsklasse, wenn sie als Meta-Prädikat fungiert (also one Vorlagenparameter). Meines Wissens nach ist die Terminologie jedoch nicht klar definiert.

Als Beispiel für die Verwendung einer Traits-Klasse in der C++-Standardbibliothek sehen Sie sich an, wie die Input/Output-Bibliothek und die String-Bibliothek entworfen sind.

Eine Richtlinie ist etwas etwas anderes (eigentlich ziemlich anders). Es soll normalerweise eine Klasse sein, die angibt, wie sich eine andere, generische Klasse in Bezug auf bestimmte Operationen verhalten soll, die möglicherweise auf verschiedene Arten realisiert werden könnten (und deren Implementierung daher der Richtlinienklasse überlassen bleibt).

Beispielsweise könnte eine generische Smart-Pointer-Klasse als Vorlagenklasse entworfen werden, die eine Richtlinie als Vorlagenparameter akzeptiert, um zu entscheiden, wie mit der Ref-Zählung umgegangen werden soll. Dies ist nur ein hypothetisches, zu vereinfachendes und anschauliches Beispiel. Versuchen Sie also bitte zu abstrahieren von diesem konkreten Code und konzentrieren Sie sich auf den Mechanismus .

Das würde es dem Designer des intelligenten Zeigers ermöglichen, keine fest codierte Verpflichtung einzugehen, ob Änderungen des Referenzzählers Thread-sicher durchgeführt werden sollen oder nicht:

template<typename T, typename P>
class smart_ptr : protected P
{
public:
    // ... 
    smart_ptr(smart_ptr const& sp)
        :
        p(sp.p),
        refcount(sp.refcount)
    {
        P::add_ref(refcount);
    }
    // ...
private:
    T* p;
    int* refcount;
};

In einem Kontext mit mehreren Threads könnte ein Client eine Instanziierung der Smart-Pointer-Vorlage mit einer Richtlinie verwenden, die Thread-sichere Inkremente und Dekremente des Referenzzählers realisiert (Windows-Plattform wird hier angenommen):

class mt_refcount_policy
{
protected:
    add_ref(int* refcount) { ::InterlockedIncrement(refcount); }
    release(int* refcount) { ::InterlockedDecrement(refcount); }
};

template<typename T>
using my_smart_ptr = smart_ptr<T, mt_refcount_policy>;

In einer Singlethread-Umgebung hingegen könnte ein Client die Smart-Pointer-Vorlage mit einer Richtlinienklasse instanziieren, die einfach den Wert des Zählers erhöht und verringert:

class st_refcount_policy
{
protected:
    add_ref(int* refcount) { (*refcount)++; }
    release(int* refcount) { (*refcount)--; }
};

template<typename T>
using my_smart_ptr = smart_ptr<T, st_refcount_policy>;

Auf diese Weise hat der Bibliotheksdesigner eine flexible Lösung bereitgestellt, die in der Lage ist, den besten Kompromiss zwischen Leistung und Sicherheit zu bieten ("Sie zahlen nicht für das, was Sie nicht nutzen"). ).


Wenn Sie ModeT, IsReentrant und IsAsync verwenden, um das Verhalten des Servers zu steuern, handelt es sich um eine Richtlinie.

Wenn Sie alternativ die Merkmale des Servers für ein anderes Objekt beschreiben möchten, können Sie eine Eigenschaftsklasse wie folgt definieren:

template <typename ServerType>
class ServerTraits;

template<>
class ServerTraits<Server>
{
    enum { ModeT = SomeNamespace::MODE_NORMAL };
    static const bool IsReentrant = true;
    static const bool IsAsync = true;
}