39 votes

pourquoi std::any_cast ne supporte pas la conversion implicite ?

Pourquoi est-ce que std::any_cast lancer 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 permettre une conversion implicite (dans le cas où le type exact que l'on veut convertir n'est pas le même que celui que l'on veut convertir) ? std::any est inconnu) ?

1 votes

Ce n'est ni possible ni nécessaire ! En effet, d'autres langues ont exactement la même restriction pour l'unboxing . Je ne peux pas penser à un seul scénario où cela serait problématique.

49voto

StoryTeller Points 6139

std::any_cast est spécifié en termes de typeid . Je cite Référence cpp sur ce sujet :

Jette std::bad_any_cast si le typeid de la demande ValueType Est-ce que ne correspond pas à celui du contenu de l'opérande.

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

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

0 votes

Ok, cela a du sens si c'est lié au typeid et si je comprends votre édition, il n'est pas possible de faire une solution de contournement sans connaître le type exact ?

6 votes

@Timo - Ce n'est pas possible ? Si vous avez une idée de comment, ce serait une avancée majeure. Je n'ai pas connaissance d'une solution de contournement. Pour tenter la conversion, vous devez obtenir une référence du bon type pour l'objet effacé. Mais pour l'obtenir, vous devez connaître le type (statiquement) au préalable. Plus on y pense, plus c'est un problème de poule et d'œuf. Le C++ ne supporte rien de tel actuellement.

0 votes

Il est certainement possible de créer un std::any avec un contrat plus large. Par exemple, tout dérivé vers la base ou tout croisement pourrait être traité sur la base des éléments suivants dynamic_cast et toute conversion primitive pourrait être codée en dur. Alors que c'est souhaitable est une toute autre question, cependant, notamment parce qu'il ne serait pas aussi bon marché que de simplement vérifier une typeid .

20voto

Yakk Points 31636

Pour faire ce que vous voulez, vous auriez besoin d'un code complet de réflexion et de réification. Cela signifie que chaque détail de chaque type devrait être enregistré dans chaque binaire (et chaque signature de chaque fonction sur chaque type ! ), et lorsque vous demandez de convertir un any en un type X, vous passez les données de X dans le any, qui contient suffisamment d'informations sur le type qu'il contient pour tenter de compiler une conversion vers X et échouer ou non.

Il existe des langages qui peuvent le faire ; chaque binaire est livré avec un bytecode IR (ou source brute) et un interpréteur/compilateur. Ces langages ont tendance à être deux fois plus lents que le C++ pour la plupart des tâches et ont une empreinte mémoire beaucoup plus importante. Il est peut-être possible d'avoir ces fonctionnalités sans ce coût, mais personne n'a ce langage à ma connaissance.

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

0 votes

Les langages qui le font utilisent généralement la compilation JIT pour atteindre des vitesses comparables à celles du C++. L'avantage du C++ (et de Rust, etc.) est qu'il est prévisible et déterministe (pour la machine, pas pour le développeur - si vous n'avez pas de comportement indéfini) et strict - ne pas faire de GC signifie que vous avez strictement plus de contrôle sur les allocations par exemple.

0 votes

@BenjaminGruenbaum Je suis d'accord : un facteur de 2 est "comparable". Il est peut-être possible d'écrire un langage qui fait tout ce qui précède et qui n'a pas un ralentissement de ~2x, mais je ne l'ai pas encore rencontré. Et je comprends que le JIT peut gérer certains microbenchmarks et égaler la vitesse du C++ dans certains cas. Il est possible que ce soit l'utilisation quasi uniforme de GC dans les langages JIT qui constitue le véritable ralentissement, et qu'un langage JIT sans GC puisse gérer la réflexion/réification complète de la base de code sans perte de performance. Indiquez-moi ce langage, j'adorerais l'examiner.

5voto

Rakete1111 Points 10248

std::any a à mettre en œuvre avec l'effacement de type. C'est parce qu'il peut stocker cualquier et ne peut pas être un modèle. Il n'y a tout simplement aucune autre fonctionnalité en C++ pour réaliser cela pour le moment.

Ce que cela signifie, c'est que std::any stockera un pointeur effacé par type, void* y std::any_cast va convertir ce pointeur au type spécifié et c'est tout. Il effectue juste un contrôle d'intégrité en utilisant typeid avant de vérifier si le type sur lequel vous l'avez coulé est celui stocké dans le any.

Autoriser les conversions implicites serait impossible avec l'implémentation actuelle. Pensez-y (ignorez le typeid vérifier pour l'instant).

std::any_cast<long>(a);

a enregistre un int et non un long . Comment std::any vous le savez ? Il peut juste lancer son void* au type spécifié, le déréférencer et le retourner. Le transfert d'un pointeur d'un type à l'autre est une violation stricte de l'aliasing et donne lieu à un 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. On ne peut pas stocker les types en C++ pour le moment. Il pourrait maintenir une liste de types avec leurs noms respectifs. typeid et passer de l'un à l'autre pour obtenir le type actuel et effectuer la conversion implicite. Mais il n'y a aucun moyen de faire cela pour cada le type unique que vous allez utiliser. Les types définis par l'utilisateur ne fonctionneraient pas de toute façon, et vous devriez compter sur des choses telles que les macros pour "enregistrer" votre type et générer le cas de commutation approprié pour celui-ci. 1 .

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, et 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 de réaliser cela 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 éventuels d'une telle approche ne valent pas la peine de s'y attarder.

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, mettre en œuvre std::any (ou un type comparable) et std::any_cast vous-même en utilisant l'approche du registre macro mentionnée ci-dessus 1 . Mais je ne le recommanderai pas. Si vous n'avez pas et ne pouvez pas savoir quel type std::any et que vous 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 doué pour l'(ab)utilisation des macros. Vous pouvez également coder en dur vos types pour votre implémentation personnalisée ou utiliser un outil séparé pour cela.

2 votes

Ce genre de trucage de casting avec l'enregistrement est certainement possible. Je l'ai fait. Mais le code résultant est très certainement pas le genre de code que l'on s'attend à trouver dans une fonction standard du langage. Il ressemble plutôt à une fonction spéciale spécifique à un domaine (ce qui est exactement ce qu'il était lorsque je l'ai implémenté)

1voto

user1095108 Points 3249

Cela pourrait être mis en œuvre 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 donc une violation de la règle de l'art. "ne pas payer pour ce que vous n'utilisez pas" principe. Un autre site any est l'incapacité de stocker un tableau.

std::any("blabla");

fonctionnera, mais il stockera un char const* et non un tableau. Vous pouvez ajouter une telle fonctionnalité dans votre propre version personnalisée. any mais il faudrait alors stocker un pointeur sur une chaîne littérale :

any(&*"blabla");

ce qui est un peu bizarre. Les décisions du comité des normes 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 le stocker et ensuite invoquer l'effacement de type foncteurs par exemple, mais cela n'est pas non plus pris en charge par la norme.

0voto

alfC Points 881

Cette question n'est pas bien posée ; les conversions implicites au bon type sont en principe possible, mais désactivé. Cette restriction existe probablement pour maintenir un certain niveau de sécurité ou pour imiter le cast explicite nécessaire (à travers le pointeur) dans la version C de any ( void* ). (L'exemple de mise en œuvre 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 travail :

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 possible (mais peut échouer au moment de 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" );
 }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X