wake-up-neo.com

Warum unterstützt std :: any_cast keine implizite Konvertierung?

Warum tut std::any_cast Werfen Sie einen std::bad_any_cast Ausnahme, wenn eine implizite Konvertierung vom tatsächlich gespeicherten Typ zum angeforderten Typ möglich wäre?

Beispielsweise:

std::any a = 10;  // holds an int now
auto b = std::any_cast<long>(a);   // throws bad_any_cast exception

Warum ist dies nicht zulässig und gibt es eine Problemumgehung, um eine implizite Konvertierung zu ermöglichen (für den Fall, dass der genaue Typ std::any hält ist unbekannt)?

33
Timo

std::any_cast wird in typeid angegeben. Um cppreference zu zitieren:

Wirft std::bad_any_cast wenn das typeid des angeforderten ValueType nicht mit dem Inhalt des Operanden übereinstimmt.

Da typeid es der Implementierung nicht erlaubt, "herauszufinden", dass eine implizite Konvertierung möglich ist, gibt es meines Wissens keine Möglichkeit, dass any_cast kann wissen, dass es auch möglich ist.

Um es anders auszudrücken, die Art der Löschung durch std::any stützt sich auf Informationen, die nur zur Laufzeit verfügbar sind. Und diese Informationen sind nicht ganz so umfangreich wie die Informationen, die der Compiler zum Ermitteln von Konvertierungen hat. Das sind die Kosten für die Typlöschung in C++ 17.

44
StoryTeller

Um das zu tun, was Sie wollen, müssen Sie den Code vollständig reflektieren und überprüfen. Das bedeutet, dass jedes Detail jedes Typs in jeder Binärdatei gespeichert werden muss (und jede Signatur jeder Funktion in jedem Typ! Und jede Vorlage überall!) Und wenn Sie nach einer Konvertierung von einem beliebigen Typ in einen Typ X fragen, den Sie übergeben würden die Daten über X in any, die genügend Informationen über den darin enthaltenen Typ enthalten, um im Grunde zu versuchen, eine Konvertierung nach X zu kompilieren und fehlzuschlagen oder nicht.

Es gibt Sprachen, die das können. Jede Binärdatei wird mit einem IR-Bytecode (oder einer Rohquelle) und einem Interpreter/Compiler geliefert. Diese Sprachen sind bei den meisten Aufgaben in der Regel 2x oder langsamer als C++ und haben einen erheblich größeren Speicherbedarf. Es mag möglich sein, diese Funktionen ohne diese Kosten zu haben, aber niemand hat diese Sprache, die ich kenne.

C++ hat diese Fähigkeit nicht. Stattdessen werden fast alle Fakten zu Typen beim Kompilieren vergessen. Für jede erinnert es sich an eine Typ-ID, mit der es eine genaue Übereinstimmung erhalten kann, und wie sein Speicher in die genaue Übereinstimmung umgewandelt wird.

std::anymuss mit Typlöschung implementiert werden. Das liegt daran, dass es beliebige Typen speichern kann und keine Vorlage sein kann. Momentan gibt es in C++ einfach keine andere Funktionalität, um dies zu erreichen.

Was das bedeutet ist, dass std::any speichert einen vom Typ gelöschten Zeiger, void* und std::any_cast konvertiert diesen Zeiger in den angegebenen Typ und das wars. Es wird lediglich eine Überprüfung der Vernunft mit typeid durchgeführt, um zu überprüfen, ob der Typ, für den Sie es umgewandelt haben, in any gespeichert ist.

Das Zulassen impliziter Konvertierungen wäre mit der aktuellen Implementierung nicht möglich. Denken Sie darüber nach (ignorieren Sie vorerst den typeid - Check).

std::any_cast<long>(a);

a speichert ein int und kein long. Wie sollte std::any wissen das? Es kann nur sein void* auf den angegebenen Typ dereferenzieren und zurückgeben. Das Umwandeln eines Zeigers von einem Typ in einen anderen stellt eine strikte Aliasing-Verletzung dar und führt zu UB. Das ist also eine schlechte Idee.

std::any müsste den tatsächlichen Typ des darin gespeicherten Objekts speichern, was nicht möglich ist. Sie können derzeit keine Typen in C++ speichern. Es könnte eine Liste von Typen zusammen mit ihren jeweiligen typeids führen und diese umschalten, um den aktuellen Typ abzurufen und die implizite Konvertierung durchzuführen. Aber es gibt keine Möglichkeit, dies für jeden einzelnen Typ zu tun, den Sie verwenden werden. Benutzerdefinierte Typen würden sowieso nicht funktionieren, und Sie müssten sich auf Dinge wie Makros verlassen, um Ihren Typ zu "registrieren" und den entsprechenden Schalterfall dafür zu generieren1.

Vielleicht so etwas:

template<typename T>
T any_cast(const any &Any) {
  const auto Typeid = Any.typeid();
  if (Typeid == typeid(int))
    return *static_cast<int *>(Any.ptr());
  else if (Typeid == typeid(long))
    return *static_cast<long *>(Any.ptr());
  // and so on. Add your macro magic here.

  // What should happen if a type is not registered?
}

Ist das eine gute Lösung? Nein, bei weitem nicht. Der Wechsel ist kostspielig, und das Mantra von C++ lautet: "Sie zahlen nicht für das, was Sie nicht verwenden", also nein, es gibt derzeit keine Möglichkeit, dies zu erreichen. Der Ansatz ist auch "hacky" und sehr spröde (was passiert, wenn Sie vergessen, einen Typ zu registrieren). Kurz gesagt, die möglichen Vorteile von so etwas sind die Mühe überhaupt nicht wert.

gibt es eine Problemumgehung, um eine implizite Konvertierung zu ermöglichen (falls der genaue Typ, den std :: any enthält, unbekannt ist)?

Ja, implementiere std::any (oder ein vergleichbarer Typ) und std::any_cast Sie verwenden den oben genannten Makro-Register-Ansatz1. Ich werde es jedoch nicht empfehlen. Wenn Sie nicht wissen und nicht wissen können, welche Art std::any speichert und muss darauf zugegriffen werden, liegt möglicherweise ein Designfehler vor.


1: Weiß eigentlich nicht, ob das möglich ist, ich bin nicht so gut in Makro (ab) verwenden. Sie können Ihre Eingaben auch für Ihre benutzerdefinierte Implementierung fest codieren oder ein separates Tool dafür verwenden.

3
Rakete1111

Dies könnte implementiert werden, indem eine implizite Kontingenzkonvertierung versucht wird, wenn die Typ-ID des angeforderten Typs nicht mit der Typ-ID des gespeicherten Typs übereinstimmt. Aber es würde Kosten verursachen und somit das "Zahlen Sie nicht für das, was Sie nicht benutzen" Prinzip verletzen. Ein weiteres Manko von any ist beispielsweise die Unfähigkeit, ein Array zu speichern.

std::any("blabla");

wird funktionieren, aber es wird ein char const* gespeichert, kein Array. Sie könnten eine solche Funktion in Ihrem eigenen angepassten any hinzufügen, aber dann müssten Sie einen Zeiger auf ein String-Literal speichern, indem Sie Folgendes tun:

any(&*"blabla");

das ist irgendwie seltsam. Die Entscheidungen des Standardkomitees sind ein Kompromiss und stellen niemals jedermann zufrieden, aber Sie haben glücklicherweise die Möglichkeit, Ihre eigenen any zu implementieren.

Sie können beispielsweise auch any zum Speichern und anschließenden Aufrufen von type-erased functors erweitern, was vom Standard jedoch ebenfalls nicht unterstützt wird.

1
user1095108

Diese Frage ist nicht gut gestellt; implizite Konvertierung in den richtigen Typ sind im Prinzip möglich, aber deaktiviert. Diese Einschränkung besteht wahrscheinlich, um ein gewisses Maß an Sicherheit aufrechtzuerhalten oder die explizite Besetzung nachzuahmen, die in der C-Version von any (void*). (Die folgende Beispielimplementierung zeigt, dass dies möglich ist.)

Abgesehen davon funktioniert Ihr Zielcode immer noch nicht, da Sie den genauen Typ vor der Konvertierung kennen müssen, aber dies kann im Prinzip funktionieren:

any a = 10;  // holds an int now
long b = int(a); // possible but today's it should be: long b = any_cast<int>(a);

Um zu zeigen, dass implizite Konvertierungen technisch möglich sind (aber zur Laufzeit fehlschlagen können):

#include<boost/any.hpp>

struct myany : boost::any{
    using boost::any::any;
    template<class T> operator T() const{return boost::any_cast<T>(*this);}
};

int main(){

    boost::any ba = 10;
//  int bai = ba; // error, no implicit conversion

    myany ma = 10; // literal 10 is an int
    int mai = ma; // implicit conversion is possible, other target types will fail (with an exception)
    assert(mai == 10);

    ma = std::string{"hello"};
    std::string mas = ma;
    assert( mas == "hello" );
 }
0
alfC