Alles, was Sie über std :: any wissen müssen

Hallo Habr! Wir präsentieren Ihnen eine Übersetzung des Artikels „Alles, was Sie über std :: any aus C ++ 17 wissen müssen“ von Bartlomiej Filipek .

Bild

Mit Hilfe von können std::optionalSie eine Art von Typ speichern. Mit Hilfe von können std::variantSie mehrere Typen in einem Objekt speichern. Und C ++ 17 bietet uns einen weiteren solchen Wrapper-Typ, std::anyder alles speichern kann, während er typsicher bleibt.

Die Grundlagen


Zuvor bot der C ++ - Standard nicht viele Lösungen für das Problem, mehrere Typen in einer Variablen zu speichern. Natürlich können Sie verwenden void*, aber es ist überhaupt nicht sicher.

Theoretisch können void*Sie es in eine Klasse einschließen, in der Sie den Typ irgendwie speichern können:

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

Wie Sie sehen können, haben wir eine bestimmte Grundform std::any, aber um die Typensicherheit zu gewährleisten MyAny, benötigen wir zusätzliche Überprüfungen. Aus diesem Grund ist es besser, eine Option aus der Standardbibliothek zu verwenden, als eine eigene Entscheidung zu treffen.

Und das ist es std::anyaus C ++ 17. Sie können alles im Objekt speichern und einen Fehler melden (eine Ausnahme auslösen), wenn Sie versuchen, durch Angabe des falschen Typs darauf zuzugreifen.

Kleine Demo:

std::any a(12);

//    :
a = std::string("Hello!");
a = 16;
//   :

//    a  
std::cout << std::any_cast<int>(a) << '\n'; 

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

//        - :
a.reset();
if (!a.has_value())
{
    std::cout << "a is empty!" << "\n";
}

//    any  :
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";
}

Dieser Code gibt Folgendes aus:

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

Das obige Beispiel zeigt einige wichtige Dinge:

  • std::anystd::optional std::variant
  • , .has_value()
  • .reset()
  • std::decay
  • ,
  • std::any_cast, bad_any_cast, «T»
  • .type(), std::type_info

Das obige Beispiel sieht beeindruckend aus - eine echte Typvariable in C ++! Wenn Sie JavaScript sehr mögen, können Sie sogar alle Ihre Typvariablen erstellen std::anyund C ++ als JavaScript verwenden :)

Aber vielleicht gibt es einige normale Verwendungsbeispiele?

Wann verwenden?


Obwohl es void*von mir als eine sehr unsichere Sache mit einem sehr begrenzten Anwendungsbereich wahrgenommen wird, ist es std::anyvollständig typsicher, sodass es einige gute Verwendungsmöglichkeiten bietet.

Zum Beispiel:

  • In Bibliotheken - wenn Ihre Bibliothek einige Daten speichern oder übertragen muss und Sie nicht wissen, welcher Typ diese Daten sein können
  • Beim Parsen von Dateien - wenn Sie wirklich nicht feststellen können, welche Typen unterstützt werden
  • Messaging
  • Interaktion mit der Skriptsprache
  • Erstellen eines Interpreters für eine Skriptsprache
  • Benutzeroberfläche - Felder können alles speichern

Es scheint mir, dass wir in vielen dieser Beispiele eine begrenzte Liste unterstützter Typen hervorheben können, daher ist dies std::variantmöglicherweise die bessere Wahl. Aber natürlich ist es schwierig, Bibliotheken zu erstellen, ohne die Endprodukte zu kennen, in denen sie verwendet werden. Sie wissen nur nicht, welche Typen dort gespeichert werden.

Die Demonstration zeigte einige grundlegende Dinge, aber in den folgenden Abschnitten erfahren Sie mehr darüber std::any. Lesen Sie also weiter.

Erstellen Sie std :: any


Es gibt verschiedene Möglichkeiten, ein Objekt vom Typ zu erstellen std::any:

  • Standardinitialisierung - Objekt ist leer
  • direkte Initialisierung mit Wert / Objekt
  • direkte Angabe des Objekttyps - std::in_place_type
  • mit der Hilfe std::make_any

Zum Beispiel:

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

//   :
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");

Wert ändern


Es std::anygibt zwei Möglichkeiten, den aktuell gespeicherten Wert zu ändern : Methode emplaceoder 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);

Objektlebenszyklus


Der Schlüssel zur Sicherheit std::anyist das Fehlen von Ressourcenlecks. Um dies zu erreichen, wird std::anyjedes aktive Objekt zerstört, bevor ein neuer Wert zugewiesen wird.

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

Dieser Code gibt Folgendes aus:

MyType::MyType
MyType::~MyType
100

Das Objekt wird std::anymit einem Objekt vom Typ MyType initialisiert, aber bevor ein neuer Wert (100.0f) zugewiesen wird, wird der Destruktor aufgerufen MyType.

Zugriff auf einen Wert erhalten


In den meisten Fällen haben Sie nur eine Möglichkeit, auf den Wert in std::any- zuzugreifen. std::any_castEr gibt die Werte des angegebenen Typs zurück, wenn er im Objekt gespeichert ist.

Diese Funktion ist sehr nützlich, da sie viele Verwendungsmöglichkeiten bietet:

  • Geben Sie eine Kopie des Werts zurück und beenden Sie den Vorgang std::bad_any_castbei einem Fehler
  • Geben Sie einen Link zum Wert zurück und beenden Sie ihn std::bad_any_cast bei einem Fehler
  • Geben Sie im Fehlerfall einen Zeiger auf einen Wert (konstant oder nicht) oder nullptr zurück

Siehe ein 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; // /
        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();
    }
}

Wie Sie sehen können, haben wir zwei Möglichkeiten, Fehler zu verfolgen: durch Ausnahmen ( std::bad_any_cast) oder die Rückgabe eines Zeigers (oder nullptr). Die Funktion std::any_castzum Zurückgeben von Zeigern ist überladen und markiert als noexcept.

Leistung und Speichernutzung


std::anyEs sieht aus wie ein leistungsstarkes Tool, und Sie werden es höchstwahrscheinlich zum Speichern von Daten verschiedener Typen verwenden. Aber wie hoch ist der Preis dafür?

Das Hauptproblem ist die zusätzliche Speicherzuordnung.

std::variant und std::optionalerfordert keine zusätzlichen Speicherzuordnungen, dies liegt jedoch daran, dass die im Objekt gespeicherten Datentypen im Voraus bekannt sind. std :: any verfügt nicht über solche Informationen, sodass zusätzlicher Speicher verwendet werden kann.

Wird das immer oder manchmal passieren? Welche Regeln? Wird dies auch bei einfachen Typen wie int passieren?

Mal sehen, was der Standard sagt:
Implementierungen sollten die Verwendung von dynamisch zugewiesenem Speicher für einen kleinen enthaltenen Wert vermeiden. Beispiel: Wenn das erstellte Objekt nur ein int enthält. Eine solche Kleinobjektoptimierung darf nur auf Typen T angewendet werden, für die is_nothrow_move_constructible_v wahr ist
Eine Implementierung sollte die Verwendung von dynamischem Speicher für kleine gespeicherte Daten vermeiden. Wenn beispielsweise ein Objekt erstellt wird, in dem nur int gespeichert wird. Eine solche Optimierung für kleine Objekte sollte nur auf Typen T angewendet werden, für die is_nothrow_move_constructible_v wahr ist.

Infolgedessen schlagen sie vor, Small Buffer Optimization / SBO für Implementierungen zu verwenden. Das hat aber auch einen Preis. Dies macht den Typ größer - um den Puffer abzudecken.

Schauen wir uns die Größe an std::any. Hier sind die Ergebnisse mehrerer Compiler:

Compilersizeof (any)
GCC 8.1 (Coliru)Sechszehn
Clang 7.0.0 (Wandbox)32
MSVC 2017 15.7.0 32-Bit40
MSVC 2017 15.7.0 64-Bit64

Wie Sie sehen, std::anyhandelt es sich im Allgemeinen nicht um einen einfachen Typ, der zusätzliche Kosten verursacht. Aufgrund von SBO nimmt es normalerweise viel Speicherplatz von 16 bis 32 Byte ein (in GCC oder Clang ... oder sogar 64 Byte in MSVC!).

Migration von boost :: any


boost::anyEs wurde irgendwo im Jahr 2001 eingeführt (Version 1.23.0). Darüber hinaus ist der Autor boost::any(Kevlin Henney) auch der Autor des Vorschlags std::any. Daher sind diese beiden Typen eng miteinander verbunden, die Version von STL basiert stark auf ihrem Vorgänger.

Hier sind die wichtigsten Änderungen:

FunktionBoost.Any
(1.67.0)
std :: any
Zusätzliche SpeicherzuordnungJaJa
Optimierung kleiner ObjekteNeinJa
platzierenNeinJa
in_place_type_t im KonstruktorNeinJa


Der Hauptunterschied besteht darin, dass boost::anySBO nicht verwendet wird, sodass deutlich weniger Speicher belegt wird (in GCC8.1 beträgt die Größe 8 Byte). Aus diesem Grund wird der Speicher auch für so kleine Typen wie int dynamisch zugewiesen.

Beispiele für die Verwendung von std :: any


Das Hauptplus std::anyist die Flexibilität. In den folgenden Beispielen sehen Sie einige Ideen (oder bestimmte Implementierungen), bei denen die Verwendung std::anydie Anwendung etwas vereinfacht.

Dateianalyse


In den Beispielen zu std::variant ( Sie können sie hier sehen [eng] ) können Sie sehen, wie Sie Konfigurationsdateien analysieren und das Ergebnis in einer Typvariablen speichern können std::variant. Jetzt schreiben Sie eine sehr allgemeine Lösung, vielleicht ist sie Teil einer Bibliothek, dann kennen Sie möglicherweise nicht alle möglichen Arten von Typen.

Das Speichern von Daten mithilfe std::anyvon Parametern ist in Bezug auf die Leistung wahrscheinlich recht gut und bietet Ihnen gleichzeitig die Flexibilität der Lösung.

Messaging


In Windows Api, das hauptsächlich in C geschrieben ist, gibt es ein Nachrichtensystem, das die Nachrichten-ID mit zwei optionalen Parametern verwendet, in denen Nachrichtendaten gespeichert werden. Basierend auf diesem Mechanismus können Sie WndProc implementieren, das die an Ihr Fenster gesendete Nachricht verarbeitet.

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

Tatsache ist, dass die Daten in wParamoder lParamin verschiedenen Formen gespeichert sind . Manchmal müssen Sie nur ein paar Bytes verwenden wParam.

Was ist, wenn wir dieses System so ändern, dass die Nachricht alles an die Verarbeitungsmethode übergeben 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;
};

Sie können beispielsweise eine Nachricht an das Fenster senden:

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

Ein Fenster kann auf eine Nachricht wie folgt antworten:

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

Natürlich müssen Sie festlegen, wie der Datentyp in Nachrichten gespeichert wird, aber jetzt können Sie echte Typen anstelle verschiedener Tricks mit Zahlen verwenden.

Eigenschaften


Das Originaldokument, das any für C ++ (N1939) darstellt, zeigt ein Beispiel für ein Eigenschaftsobjekt:

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

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

typedef std::vector<property> properties;

Dieses Objekt sieht sehr nützlich aus, da es viele verschiedene Typen speichern kann. Das erste, was mir in den Sinn kommt, ist ein Beispiel für die Verwendung in einem Benutzeroberflächen-Manager oder in einem Spieleditor.

Wir gehen durch die Grenzen


In r / cpp gab es einen Stream über std :: any. Und es gab mindestens einen großartigen Kommentar, der zusammenfasst, wann ein Typ verwendet werden sollte.

Aus diesem Kommentar :
Unter dem Strich können Sie mit std :: any Rechte an beliebigen Daten über Grenzen hinweg übertragen, die ihren Typ nicht kennen.
Alles, worüber ich vorher gesprochen habe, kommt dieser Idee nahe:

  • in der Bibliothek für die Schnittstelle: Sie wissen nicht, welche Typen der Client dort verwenden möchte
  • Messaging: die gleiche Idee - geben Sie dem Kunden Flexibilität
  • Dateianalyse: Zur Unterstützung eines beliebigen Typs

Gesamt


In diesem Artikel haben wir viel gelernt std::any!

Hier sind einige Dinge zu beachten:

  • std::any keine Vorlagenklasse
  • std::any Verwendet die Optimierung kleiner Objekte, sodass für solche einfachen Typen wie int oder double kein Speicher dynamisch zugewiesen wird. Für größere Typen wird zusätzlicher Speicher verwendet
  • std::any kann als "schwer" bezeichnet werden, bietet aber Sicherheit und größere Flexibilität
  • Der Zugriff auf die Daten std::anykann mit Hilfe von erfolgen any_cast, das mehrere "Modi" bietet. Im Fehlerfall kann beispielsweise eine Ausnahme ausgelöst oder nur nullptr zurückgegeben werden
  • Verwenden Sie es, wenn Sie nicht genau wissen, welche Datentypen möglich sind. Andernfalls sollten Sie es verwenden std::variant

All Articles