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 .
Mit Hilfe von können std::optional
Sie eine Art von Typ speichern. Mit Hilfe von können std::variant
Sie mehrere Typen in einem Objekt speichern. Und C ++ 17 bietet uns einen weiteren solchen Wrapper-Typ, std::any
der 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::any
aus 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;
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";
}
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::any
— std::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::any
und 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::any
vollstä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::variant
mö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);
std::any a3(MyType(10, 11));
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};
std::any a6 = std::make_any<std::string>("Hello World");
Wert ändern
Es std::any
gibt zwei Möglichkeiten, den aktuell gespeicherten Wert zu ändern : Methode emplace
oder 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::any
ist das Fehlen von Ressourcenlecks. Um dies zu erreichen, wird std::any
jedes 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::any
mit 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_cast
Er 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_cast
bei 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);
}
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_cast
zum Zurückgeben von Zeigern ist überladen und markiert als noexcept
.Leistung und Speichernutzung
std::any
Es 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::optional
erfordert 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:Wie Sie sehen, std::any
handelt 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::any
Es 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:Der Hauptunterschied besteht darin, dass boost::any
SBO 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::any
ist die Flexibilität. In den folgenden Beispielen sehen Sie einige Ideen (oder bestimmte Implementierungen), bei denen die Verwendung std::any
die 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::any
von 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 wParam
oder lParam
in 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 Vorlagenklassestd::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 verwendetstd::any
kann als "schwer" bezeichnet werden, bietet aber Sicherheit und größere Flexibilität- Der Zugriff auf die Daten
std::any
kann 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