Alles, was Sie über std::any von C++17 wissen müssen

Alles, was Sie über std::any von C++17 wissen müssen

Mit std::optional Sie können einen Typ oder nichts darstellen. Mitstd::variant Sie können mehrere Varianten in eine Entität packen. Und C++17 gibt uns einen weiteren Wrapper-Typ:std::any die alles typsicher aufnehmen kann.

Die Grundlagen

Bisher hatten Sie im Standard-C++ nicht viele Optionen, wenn es darum geht, Variablentypen in einer Variablen zu halten. Natürlich könnten Sie void* verwenden ,aber das war nicht supersicher.

Möglicherweise void* könnte mit einem Typdiskriminator in eine Klasse eingeschlossen werden.

class MyAny
{
    void* _value;
    TypeInfo _typeInfo;
};

Wie Sie sehen, haben wir eine Grundform des Typs, aber es ist ein bisschen Codierung erforderlich, um sicherzustellen, dass MyAny ist typsicher. Aus diesem Grund ist es am besten, die Standardbibliothek zu verwenden, anstatt eine benutzerdefinierte Implementierung zu erstellen.

Und das ist was std::any von C++17 ist in seiner Grundform. Es gibt Ihnen die Möglichkeit, alles in einem Objekt zu speichern, und es meldet Fehler (oder löst Ausnahmen aus), wenn Sie auf einen Typ zugreifen möchten, der nicht aktiv ist.

Eine kleine Demo:

std::any a(12);

// set any value:
a = std::string("Hello!");
a = 16;
// reading a value:

// we can read it as int
std::cout << std::any_cast<int>(a) << '\n'; 

// but not as string:
try 
{
    std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e) 
{
    std::cout << e.what() << '\n';
}

// reset and check if it contains any value:
a.reset();
if (!a.has_value())
{
    std::cout << "a is empty!" << "\n";
}

// you can use it in a container:
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;

for (auto &[key, val] : m)
{
    if (val.type() == typeid(int))
        std::cout << "int: " << std::any_cast<int>(val) << "\n";
    else if (val.type() == typeid(std::string))
        std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
    else if (val.type() == typeid(float))
        std::cout << "float: " << std::any_cast<float>(val) << "\n";
}

Der Code gibt aus:

16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World

Spielen Sie mit dem Code @Coliru

Wir haben im obigen Beispiel mehrere Dinge gezeigt:

  • std::any ist keine Template-Klasse wie std::optional oderstd::variant .
  • standardmäßig enthält es keinen Wert, und Sie können es über .has_value() überprüfen .
  • Sie können einen any zurücksetzen Objekt über .reset() .
  • es funktioniert mit „zerfallenen“ Typen – also wird der Typ vor der Zuweisung, Initialisierung, Einlagerung durch std::decay transformiert.
  • Wenn ein anderer Typ zugewiesen wird, wird der aktive Typ zerstört.
  • Sie können auf den Wert zugreifen, indem Sie std::any_cast<T> verwenden , wird bad_any_cast ausgelöst wenn der aktive Typ nicht T ist .
  • Sie können den aktiven Typ ermitteln, indem Sie .type() verwenden das gibt std::type_infodes Typs zurück.

Das obige Beispiel sieht beeindruckend aus - ein echter Variablentyp in C++!. Wenn Sie JavaScript mögen, können Sie sogar alle Ihre Variablen erstellenstd::any und C++ wie JavaScript verwenden :)

Aber vielleicht gibt es einige legitime Anwendungsfälle?

Wann zu verwenden

Während ich void* wahrnehme als extrem unsicheres Muster mit einigen begrenzten Anwendungsfällen, std::any fügt Typsicherheit hinzu und hat daher einige echte Anwendungsfälle.

Einige Möglichkeiten:

  • In Bibliotheken - wenn ein Bibliothekstyp irgendetwas enthalten oder weitergeben muss, ohne die Menge der verfügbaren Typen zu kennen.
  • Parsing-Dateien - wenn Sie die unterstützten Typen wirklich nicht angeben können.
  • Nachrichtenübergabe.
  • Bindungen mit einer Skriptsprache.
  • Implementieren eines Interpreters für eine Skriptsprache
  • Benutzeroberfläche - Steuerelemente können alles enthalten
  • Entitäten in einem Editor

Ich glaube, dass wir in vielen Fällen die Menge der unterstützten Typen einschränken können, und deshalb std::variant könnte die bessere Wahl sein. Natürlich wird es schwierig, wenn Sie eine Bibliothek implementieren, ohne die endgültigen Anwendungen zu kennen – Sie kennen also nicht die möglichen Typen, die in einem Objekt gespeichert werden.

Die Demo zeigte einige Grundlagen, aber in den folgenden Abschnitten entdecken Sie weitere Details zu std::any also lies weiter.

Die Serie

Dieser Artikel ist Teil meiner Serie über C++17 Library Utilities. Hier ist die Liste der anderen Themen, die ich behandeln werde:

  • Refaktorisierung mit std::optional
  • Mitstd::optional
  • Fehlerbehandlung undstd::optional
  • Überstd::variant
  • Mit std::any (dieser Beitrag)
  • Bestandsbau für std::optional , std::variant undstd::any
  • Mit std::string_view
  • C++17-Stringsucher und Konvertierungsprogramme
  • Arbeiten mit std::filesystem
  • Noch etwas?
    • Zeigen Sie mir Ihren Code:std::optional
    • Ergebnisse:Zeig mir deinen Kern:std::optional
    • Menu-Klasse – Beispiel für moderne C++17-STL-Funktionen

Ressourcen zu C++17 STL:

  • C++17 im Detail von Bartek!
  • C++17 – Der vollständige Leitfaden von NicolaiJosuttis
  • C++-Grundlagen einschließlich C++17 von Kate Gregory
  • Praktische C++14- und C++17-Funktionen – von Giovanni Dicanio
  • C++17-STL-Kochbuch von Jacek Galowicz

std::any Erstellung

Es gibt mehrere Möglichkeiten, std::any zu erstellen Objekt:

  • eine Default-Initialisierung - dann ist das Objekt leer
  • eine direkte Initialisierung mit einem Wert/Objekt
  • statt std::in_place_type
  • über std::make_any

Sie können es im folgenden Beispiel sehen:

// default initialization:
std::any a;
assert(!a.has_value());

// initialization with an object:
std::any a2(10); // int
std::any a3(MyType(10, 11));

// in_place:
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};

// make_any
std::any a6 = std::make_any<std::string>("Hello World");

Spielen Sie mit dem Code @Coliru

Ändern des Werts

Wenn Sie den aktuell gespeicherten Wert in std::any ändern möchten Dann haben Sie zwei Möglichkeiten:Verwenden Sie emplace oder die Zuweisung:

std::any a;

a = MyType(10, 11);
a = std::string("Hello");

a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);

Spielen Sie mit dem Code @Coliru

Objektlebensdauer

Der entscheidende Teil der Sicherheit für std::any ist, keine Ressourcen zu verlieren. Um dieses Verhalten zu erreichen std::any zerstört jedes aktive Objekt, bevor es einen neuen Wert zuweist.

std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";

Spielen Sie mit dem Code @Coliru

Dies erzeugt die folgende Ausgabe:

MyType::MyType
MyType::~MyType
100

Das beliebige Objekt wird mit MyType initialisiert , aber bevor es einen neuen Wert erhält (von 100.0f ) ruft es den Destruktor von MyType auf .

Zugriff auf den gespeicherten Wert

Um den aktuell aktiven Wert in std::any auszulesen Sie haben meistens eine Option:std::any_cast . Diese Funktion gibt den Wert des angeforderten Typs zurück, wenn er im Objekt enthalten ist.

Diese Funktionsvorlage ist jedoch ziemlich leistungsfähig, da sie viele Verwendungsmöglichkeiten bietet:

  • um eine Kopie des Werts zurückzugeben und std::bad_any_cast auszulösen wenn es fehlschlägt
  • um eine Referenz (auch beschreibbar) zurückzugeben und std::bad_any_cast auszulösen wenn es fehlschlägt
  • um einen Zeiger auf den Wert (konstant oder nicht) oder nullptr zurückzugeben bei Ausfall

Siehe Beispiel

struct MyType
{
    int a, b;

    MyType(int x, int y) : a(x), b(y) { }

    void Print() { std::cout << a << ", " << b << "\n"; }
};

int main()
{
    std::any var = std::make_any<MyType>(10, 10);
    try
    {
        std::any_cast<MyType&>(var).Print();
        std::any_cast<MyType&>(var).a = 11; // read/write
        std::any_cast<MyType&>(var).Print();
        std::any_cast<int>(var); // throw!
    }
    catch(const std::bad_any_cast& e) 
    {
        std::cout << e.what() << '\n';
    }

    int* p = std::any_cast<int>(&var);
    std::cout << (p ? "contains int... \n" : "doesn't contain an int...\n");

    MyType* pt = std::any_cast<MyType>(&var);
    if (pt)
    {
        pt->a = 12;
        std::any_cast<MyType&>(var).Print();
    }
}

Spielen Sie mit dem Code @Coliru

Wie Sie sehen, haben Sie zwei Möglichkeiten zur Fehlerbehandlung:über Ausnahmen (std::bad_any_cast ) oder durch Rückgabe eines Zeigers (oder nullptr ). Die Funktionsüberladungen für std::_any_cast Zeigerzugriffe sind ebenfalls mit noexcept gekennzeichnet .

Leistungs- und Speicherüberlegungen

std::any sieht ziemlich mächtig aus und Sie könnten es verwenden, um Variablen von Variablentypen zu speichern … aber Sie fragen sich vielleicht, was der Preis für eine solche Flexibilität ist?

Das Hauptproblem:zusätzliche dynamische Speicherzuweisungen.

std::variant und std::optional benötigen keine zusätzlichen Speicherzuweisungen, aber das liegt daran, dass sie wissen, welcher Typ (oder welche Typen) im Objekt gespeichert werden. std::any hat kein Wissen und könnte deshalb etwas Heap-Speicher verwenden.

Wird es immer passieren oder manchmal? Was sind die Regeln? Wird es auch bei einem einfachen Typ wie int passieren ?

Mal sehen, was der Standard sagt:

Aus dem Standard:

Zusammenfassend:Implementierungen werden ermutigt, SBO - Small BufferOptimization zu verwenden. Aber das hat auch seinen Preis:Es wird die Schrift größer machen - um in den Puffer zu passen.

Sehen wir uns an, wie groß std::any ist :

Hier sind die Ergebnisse der drei Compiler:

Spielen Sie mit code@Coliru

Im Allgemeinen, wie Sie sehen, std::any ist kein „einfacher“ Typ und bringt viel Overhead mit sich. Es ist normalerweise nicht klein - aufgrund von SBO - es dauert 16 oder 32 Bytes (GCC oder Clang ... oder sogar 64 Bytes in MSVC!)

Migration von boost::any

Boost Any wurde um das Jahr 2001 eingeführt (Version Version 1.23.0). Darüber hinaus ist der Autor der Boost-Bibliothek – Kevlin Henney – auch der Autor des Vorschlags für std::any . Die beiden Typen sind also stark miteinander verbunden, und die STL-Version basiert stark auf dem Vorgänger.

Hier sind die wichtigsten Änderungen:

Der Hauptunterschied besteht darin, dass boost.any verwendet kein SBO, also ist es ein viel kleinerer Typ (GCC8.1 meldet 8 Bytes), aber als Konsequenz wird es selbst für einfache Typen wie int einen Speicher zuweisen .

Beispiele für std::any

Der Kern von std::any ist Flexibilität. In den Beispielen unten sehen Sie also einige Ideen (oder konkrete Implementierungen), bei denen das Halten von Variablentypen eine Anwendung etwas einfacher machen kann.

Dateien parsen

In den Beispielen etwa std::variant (siehe hier) Sie konnten sehen, wie es möglich ist, Konfigurationsdateien zu parsen und das Ergebnis als Alternative zu mehreren Typen zu speichern. Wenn Sie jedoch eine wirklich generische Lösung schreiben – vielleicht als Teil einer Bibliothek, dann kennen Sie vielleicht nicht alle möglichen Typen.

Speichern von std::any als Wert für eine Eigenschaft kann aus Sicht der Leistung gut genug sein und Ihnen Flexibilität geben.

Nachrichtenübergabe

In Windows Api, das meistens C ist, gibt es ein Nachrichtenübermittlungssystem, das Nachrichten-IDs mit zwei optionalen Parametern verwendet, die den Wert der Nachricht speichern. Basierend auf diesem Mechanismus können Sie WndProc implementieren das behandelt die an Ihr Fenster/Steuerelement übergebenen Nachrichten:

LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,
  _In_ UINT   uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

Der Clou dabei ist, dass die Werte in wParam gespeichert werden oder lParam unveränderliche Formen. Manchmal müssen Sie nur wenige Bytes von wParam verwenden …

Was wäre, wenn wir dieses System in std::any ändern würden , damit eine Nachricht irgendetwas an die Behandlungsmethode weitergeben kann?

Zum Beispiel:

class Message
{
public:
    enum class Type 
    {
        Init,
        Closing,
        ShowWindow,        
        DrawWindow
    };

public:
    explicit Message(Type type, std::any param) :
        mType(type),
        mParam(param)
    {   }
    explicit Message(Type type) :
        mType(type)
    {   }

    Type mType;
    std::any mParam;
};

class Window
{
public:
    virtual void HandleMessage(const Message& msg) = 0;
};

Beispielsweise können Sie eine Nachricht an ein Fenster senden:

Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);

Dann kann das Fenster auf die Nachricht wie folgt reagieren:

switch (msg.mType) {
// ...
case Message::Type::ShowWindow:
    {
    auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
    std::cout << "ShowWidow: "
              << pos.first << ", " 
              << pos.second << "\n";
    break;
    }
}

Spielen Sie mit dem Code @Coliru

Natürlich müssen Sie definieren, wie die Werte angegeben werden (was sind die Typen von Werten einer Nachricht), aber jetzt können Sie echte Typen verwenden, anstatt verschiedene Tricks mit ganzen Zahlen zu machen.

Eigenschaften

Das Originaldokument, das any in C++ einführt, N1939, zeigt ein Beispiel einer Eigenschaftsklasse.

struct property
{
    property();
    property(const std::string &, const std::any &);

    std::string name;
    std::any value;
};

typedef std::vector<property> properties;

Die properties Das Objekt sieht sehr mächtig aus, da es viele verschiedene Typen enthalten kann. Als erster Anwendungsfall kommt mir ein generischer UI-Manager oder ein Spieleditor in den Sinn.

Grenzen überschreiten

Vor einiger Zeit gab es einen Thread zu [r/cpp](
https://www.reddit.com/r/cpp/comments/7l3i19/why_was_stdany_added_to_c17/
) über std::any . Und es gab mindestens einen großartigen Kommentar, der zusammenfasst, wann der Typ verwendet werden sollte:

Aus dem Kommentar:

Alles, was ich zuvor erwähnt habe, kommt dieser Idee nahe:

  • in einer UI-Bibliothek:Sie wissen nicht, was die endgültigen Typen sind, die ein Client verwenden könnte
  • Message Passing:Gleiche Idee, Sie möchten die Flexibilität für den Kunden haben
  • Parsing-Dateien:Um benutzerdefinierte Typen zu unterstützen, könnte ein wirklich „variabler“ Typ nützlich sein

Entschuldigung für die kleine Unterbrechung im Fluss :)
Ich habe einen kleinen Bonus vorbereitet, falls Sie an C++17 interessiert sind, sehen Sie sich das hier an:

Laden Sie eine kostenlose Kopie der C++17 Language RefCard herunter!

Abschluss

In diesem Artikel haben wir viel über std::any behandelt !

Hier sind die Dinge, die Sie bei std::any beachten sollten :

  • std::any ist keine Template-Klasse
  • std::any verwendet Small Buffer Optimization, also wird Speicher für einfache Typen wie ints, doubles ... nicht dynamisch zugewiesen, aber für größere Typen wird zusätzliches new verwendet .
  • std::any mag als „schwer“ gelten, bietet aber viel Flexibilität und Typsicherheit.
  • mit any_cast können Sie auf den aktuell gespeicherten Wert zugreifen das bietet ein paar „Modi“:zum Beispiel könnte es eine Ausnahme auslösen oder einfach nullptr zurückgeben .
  • Verwenden Sie es, wenn Sie die möglichen Typen nicht kennen, in anderen Fällen ziehen Sie std::variant in Betracht .

Nun ein paar Fragen an Sie:

  • Haben Sie std::any verwendet? oder boost::any ?
  • Können Sie die Anwendungsfälle nennen?
  • Wo sehen Sie std::any könnte nützlich sein?

CodeProject