web-dev-qa-db-fra.com

pourquoi std :: any_cast ne prend-il pas en charge la conversion implicite?

Pourquoi std::any_cast jeter un std::bad_any_cast exception lorsqu'une conversion implicite du type stocké réel vers le type demandé serait possible?

Par exemple:

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

Pourquoi cela n'est-il pas autorisé et existe-t-il une solution de contournement pour autoriser une conversion implicite (dans le cas du type exact que std::any les prises sont inconnues)?

33
Timo

std::any_cast est spécifié en termes de typeid. Pour citer cppreference à ce sujet:

Jette std::bad_any_cast si le typeid du ValueType demandé ne correspond pas à celui du contenu de l'opérande.

Puisque typeid ne permet pas à l'implémentation de "comprendre" qu'une conversion implicite est possible, il n'y a aucun moyen (à ma connaissance) que any_cast peut savoir que c'est possible non plus.

Autrement dit, l'effacement de type fourni par std::any repose sur des informations disponibles uniquement au moment de l'exécution. Et ces informations ne sont pas aussi riches que les informations dont dispose le compilateur pour déterminer les conversions. C'est le coût de l'effacement de type en C++ 17.

44
StoryTeller

Pour faire ce que vous voulez, vous aurez besoin d'une réflexion et d'une réification complètes du code. Cela signifie que chaque détail de chaque type devrait être enregistré dans chaque fichier binaire (et chaque signature de chaque fonction sur chaque type! Et chaque modèle n'importe où!), Et lorsque vous demandez à convertir d'un any en un type X, vous passeriez les données sur X dans l'un, qui contiendrait suffisamment d'informations sur le type qu'il contenait pour tenter de compiler une conversion en X et échouer ou non.

Il existe des langues qui peuvent le faire; chaque binaire est livré avec un bytecode IR (ou source brute) et un interprète/compilateur. Ces langages ont tendance à être 2x ou plus lents que C++ dans la plupart des tâches et ont des empreintes de mémoire considérablement plus importantes. Il peut être possible d'avoir ces fonctionnalités sans ce coût, mais personne n'a cette langue que je connaisse.

C++ n'a pas cette capacité. Au lieu de cela, il oublie presque tous les faits sur les types lors de la compilation. Pour tout, il se souvient d'un typeid qu'il peut être utilisé pour obtenir une correspondance exacte et comment convertir son stockage en ladite correspondance exacte.

19

std::anyhas à implémenter avec effacement de type. C'est parce qu'il peut stocker le type any et ne peut pas être un modèle. Il n'y a pour le moment aucune autre fonctionnalité en C++.

Cela signifie que std::any stockera un pointeur effacé, void* et std::any_cast convertira ce pointeur au type spécifié et c'est tout. Il vérifie simplement la santé mentale en utilisant typeid avant de vérifier si vous le type que vous avez casté est celui stocké dans l'un.

Autoriser les conversions implicites serait impossible en utilisant l'implémentation actuelle. Pensez-y (ignorez la vérification de typeid pour l'instant).

std::any_cast<long>(a);

a stocke un int et non un long. Comment doit std::any sache que? Il peut simplement lancer son void* au type spécifié, déréférencez-le et renvoyez-le. La conversion d'un pointeur d'un type à un autre est une violation d'alias stricte et entraîne UB, c'est donc une mauvaise idée.

std::any devrait stocker le type réel de l'objet qui y est stocké, ce qui n'est pas possible. Vous ne pouvez pas stocker de types en C++ pour le moment. Il pourrait maintenir une liste de types avec leurs typeid respectifs et les basculer pour obtenir le type actuel et effectuer la conversion implicite. Mais il n'y a aucun moyen de le faire pour chaque type unique que vous allez utiliser. Les types définis par l'utilisateur ne fonctionneraient pas de toute façon, et vous devriez vous fier à des éléments tels que les macros pour "enregistrer" votre type et générer le cas de commutateur approprié pour celui-ci1.

Peut-être quelque chose comme ça:

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

Est-ce une bonne solution? Non, de loin. Le changement est coûteux, et le mantra de C++ est "vous ne payez pas pour ce que vous n'utilisez pas" donc non, il n'y a aucun moyen d'y parvenir actuellement. L'approche est également "hacky" et très fragile (que se passe-t-il si vous oubliez d'enregistrer un type). En bref, les avantages possibles de faire quelque chose comme ça ne valent pas la peine en premier lieu.

existe-t-il une solution de contournement pour permettre une conversion implicite (dans le cas où le type exact que std :: any détient est inconnu)?

Oui, implémentez std::any (ou un type comparable) et std::any_cast vous-même en utilisant l'approche du macro-registre mentionnée ci-dessus1. Je ne le recommanderai pas cependant. Si vous ne savez pas quel type std::any stocke et devez y accéder, vous avez peut-être un défaut de conception.


1: Je ne sais pas vraiment si c'est possible, je ne suis pas très bon en macro (ab). Vous pouvez également coder en dur vos types pour votre implémentation personnalisée ou utiliser un outil distinct pour cela.

3
Rakete1111

Cela pourrait être implémenté en essayant une conversion implicite de contingence, si l'ID de type du type demandé n'était pas le même que l'ID de type du type stocké. Mais cela impliquerait un coût et violerait donc le principe "ne pas payer pour ce que vous n'utilisez pas" . Un autre any défaut, par exemple, est l'impossibilité de stocker un tableau.

std::any("blabla");

fonctionnera, mais il stockera un char const*, pas un tableau. Vous pouvez ajouter une telle fonctionnalité dans votre propre any personnalisé, mais vous devrez alors stocker un pointeur sur un littéral de chaîne en procédant comme suit:

any(&*"blabla");

ce qui est un peu bizarre. Les décisions du comité Standard sont un compromis et ne satisfont jamais tout le monde, mais vous avez heureusement la possibilité de mettre en œuvre votre propre any.

Vous pouvez également étendre any pour stocker, puis invoquer le type effacé functors , par exemple, mais cela n'est pas non plus pris en charge par la norme.

1
user1095108

Cette question n'est pas bien posée; la conversion implicite au bon type est en principe possible, mais désactivée. Cette restriction existe probablement pour maintenir un certain niveau de sécurité ou pour imiter le casting explicite nécessaire (via le pointeur) dans la version C de any (void*). (L'exemple d'implémentation ci-dessous montre que c'est possible.)

Cela dit, votre code cible ne fonctionne toujours pas car vous devez connaître le type exact avant la conversion, mais cela peut en principe fonctionner:

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

Pour montrer que les conversions implicites sont techniquement possibles (mais peuvent échouer à l'exécution):

#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