1
votes

Problème de poinçonnage de type en C ++

J'ai une classe de modèle avec un booléen comme paramètre de modèle Dynamic . Que le paramètre soit vrai ou faux, il a exactement les mêmes membres de données. ils diffèrent simplement dans leurs fonctions membres.

Il y a une situation dans laquelle j'ai besoin de les convertir temporairement l'une à l'autre, au lieu d'utiliser un constructeur de copie / déplacement. J'ai donc eu recours au jeu de mots. Pour m'assurer que cela cause un problème, j'ai utilisé deux static_asserts:

warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

Donc je pense que ce que je fais est sûr, et si quelque chose est pour se tromper, le compilateur me donnera une erreur static_assert . Cependant, gcc donne un avertissement:

d_true=Dynamic<true>(...);
...
static_assert(sizeof(Dynamic<true>)==sizeof(Dynamic<false>),"Dynamic size mismatch");
static_assert(alignof(Dynamic<true>)==alignof(Dynamic<false>),"Dynamic align mismatch");
Dynamic<false>& d_false=*reinterpret_cast<Dynamic<false>*>(&d_true);
...

Ma question est double: ce que je fais est-il le meilleur moyen d'y parvenir? Si c'est le cas, comment convaincre gcc qu'il est sûr et se débarrasser de l'avertissement?


2 commentaires

Vous n'êtes pas autorisé à faire des punitions de type comme ça en C ++.


Non, ce que vous faites n'est pas le meilleur moyen d'y parvenir. La meilleure façon d'y parvenir n'implique pas de jeu de mots, mais plutôt la refonte de vos modèles et des classes associées afin que ces types de gymnastique ne soient pas nécessaires.


4 Réponses :


3
votes

Une possibilité évidente serait de séparer les données communes aux deux dans sa propre classe (ou structure), puis de les récupérer de l'objet lorsque vous en avez besoin.

struct Common { /* ... */ };

template <bool t>
class Dynamic : public Common {
    // ...
};

De là, le reste semble assez évident - quand vous avez besoin des données du Dynamic , vous appelez get_data () et c'est parti.

Bien sûr, il existe également des variantes du thème général - par exemple, vous pouvez utiliser l'héritage à la place:

struct Common {
// ...
};

template <bool b>
class Dynamic { 
    Common c;
public:
    Common &get_data() { return c; }
    // ...
};

Ceci élimine le c supplémentaire. la version précédente aurait besoin pour chaque référence aux données communes, mais (du moins à mon avis) l'héritage est probablement un prix trop élevé à payer pour cela.


0 commentaires

0
votes

Dans la norme, il est "interdit" de réinterpréter une région de mémoire du type A au type B. Cela s'appelle l'aliasing. Il existe 3 exceptions aux alias, les mêmes types avec des qualifications de CV, des types de base et des régions de char [] différents. (et pour char la dérogation ne fonctionne que de manière unidirectionnelle dans le sens de char)

Si vous utilisez std :: aligné_storage et le placement new, vous pouvez réinterpréter cette région en n'importe quoi vous voulez sans que le compilateur puisse se plaindre. C'est ainsi que fonctionne la variante .

EDIT : Ok, ce qui précède est en fait vrai (tant que vous ne l'oubliez pas std :: launder ), mais trompeur à cause de "durée de vie". Un seul objet peut vivre au-dessus d'un espace de stockage à la fois. C'est donc un comportement indéfini de l'interpréter à travers la vue d'un autre type, tant qu'il est vivant. La clé est la construction.

Si je peux suggérer, allez sur cppreference , prenez leur exemple de static_vector , simplifiez-le au cas de 1. Ajoutez quelques getters, félicitations, vous avez réinventé le bitcast :) (proposition http: //www.open- std.org/jtc1/sc22/wg21/docs/papers/2017/p0476r2.html ).

Cela ressemblerait probablement à ceci:

#include <type_traits>
#include <string>
#include <new>
#include <cstring>
#include <iostream>

using namespace std;

template< bool B >
struct Dynamic
{
    template <bool B2 = B>
    void ConditionalMethod(typename enable_if<B2>::type** = 0)
    {}

    string m_sharedObject = "stuff";
};

int main()
{
    using D0 = Dynamic<false>;
    using D1 = Dynamic<true>;
    aligned_storage<sizeof(D0), alignof(D0)>::type store[1];

    D0* inst0 = new (&store[0]) D0 ;

    // mutate
    inst0->m_sharedObject = "thing";

    // pune to D1
    D1* inst1 = std::launder(reinterpret_cast<D1*>(&store[0]));

    // observe aliasing
    cout << inst1->m_sharedObject;

    inst0->~D0();
}

voir en effet dans wandbox

EDIT: après une longue discussion il y a d'autres parties de la nouvelle norme que la section 'Types 8.2.1.11', cela explique mieux pourquoi cela n'est pas pas strictement valable. Je recommande de se référer au chapitre "durée de vie".
https://en.cppreference.com/w/cpp/language/lifetime
Du commentaire de Miles Budnek:

il n'y a pas d'objet Dynamic à cette adresse, y accéder via un Dynamic est un comportement indéfini.


37 commentaires

std :: variant ne fait pas de jeu de mots. Et les syndicats afaik n'autorisent pas le poinçonnage de type en C ++ (ils le font en C).


@bolov, il réinterprète une région de char dans le type que vous obtenez après avoir vérifié que l'index actuel correspond à ce type. ce n'est pas du jeu de mots, mais si vous voulez le punir, vous devrez passer par une région de char.


@ v.oddou Non, il ne réinterprète pas un tableau de char comme un autre type. Il utilise placement-new pour construire un nouvel objet dans un stockage non initialisé. Ce sont des choses très différentes. Le premier conduit à un comportement indéfini, tandis que le second est bien défini.


@MilesBudnek no. boost.org/doc/libs/master/boost/type_traits/ aligné_storage.‌ hpp


@ v.oddou qu'est-ce que ce lien est supposé prouver?


@ M.M que le "stockage unitaire" n'est pas très différent d'un tableau de caractères. array of char est le seul moyen de créer un espace de stockage unitialisé (en stockage automatique de durée). Je ne comprends pas l'intérêt de les "très différents". La variante utilise storage_t qui est essentiellement std :: aligné_storage avant d'être normalisé. Et comment ce storage_t est implémenté est unsigned char [sizeof (T) + alignof (T)] space et la fonction address () obtient le premier pointeur aligné de cet espace. Et ce n'est pas du tout UB puisque la norme permet une dérogation à l'aliasing pour les régions de char


Le fait est que le contenu du tableau char n'est jamais réinterprété (ce qui serait un comportement non défini). Le but du aligné_storage est que l'appelant puisse utiliser placement-new pour créer de nouveaux objets dans la même région de stockage (ce qui met fin à la durée de vie du tableau char). De plus, la norme ne permet PAS d'aliaser un tableau de caractères comme un autre type. Il permet d'aliaser d'autres types en tant que tableau de caractères.


@ M.M? pourquoi suis-je sous le feu ici, la terminologie «réinterpréter»? la source utilise en effet static_cast , ce qui en fait un concept différent? lined_storage n'est pas une primitive du langage, c'est un engin de bibliothèque, vous en conviendrez. remplacez-le mentalement par char array (+ petite aide sur l'alignement pour le type T) Qu'appelez-vous "mettre fin à la durée de vie du tableau de caractères"? Voulez-vous dire quelque chose comme le libellé standard des syndicats: "au plus un des membres de données non statiques peut être actif à tout moment"?


@ M.M "De plus, le standard ne permet PAS d'aliaser un tableau de caractères comme un autre type" source?


C'est dans la règle stricte d'aliasing dans la norme, il répertorie les types d'alias autorisés et aucun d'entre eux n'alias un char comme un type général


Placement-new met fin à la durée de vie de tout objet qui existait auparavant à l'emplacement, c'est ce que dit la norme.


@ M.M cette liste à droite stackoverflow.com/a/7005988/893406 ? "Placement-new met fin à la durée de vie du tableau de caractères"> ok je vais lire ça.


@ v.oddou source: open-std. org / jtc1 / sc22 / wg21 / docs / papers / 2017 / n4713.pdf §8.2.1 Catégorie de valeur [basic.lval] paragraphe 11. Vous pouvez accéder à un objet via une glvalue de type char, mais pas le inverse.


@bolov OOOOH c'est tout, je vois le problème ici! Ok ok ok ok, dans le cas de OP, son objet existe déjà, et donc n'a pas été réinterprété à partir d'un tableau de caractères, donc il ne peut pas être puni sans être copié. C'est pourquoi bit_cast effectue la copie de mémoire. Ok et cela explique pourquoi variant et optionnel fonctionnent, car leur stockage est la région char, dès le début. Maintenant, la question restante est si vous avez déjà puni votre personnage à T, avez-vous assez de mana restant pour le régler en T2 tant que T est vivant? Si le tableau de caractères est mort par l'effet du nouveau T (p) , cela serait compromis.


attends une minute. mais c'est ce que j'ai recommandé dans ma réponse. donc quel est le problème ?


@ v.oddou Le fait est que la variante ne réinterprète jamais rien. S'il change le type qu'il contient, il détruit d'abord l'ancien objet, puis en construit un nouveau. Les deux objets habitent le même stockage , mais seulement l'un après l'autre. Rien n'est puni ou réinterprété. OP veut traiter le même objet comme un type différent sans le copier, et vous ne pouvez tout simplement pas le faire (en dehors de circonstances très limitées, dont aucune n'implique de fonctions membres).


@MilesBudnek ok, alors vous dites que l'idée présentée dans le code dans ma réponse est illégale, non? Je suis ouvert à l'accepter, mais cela ne semble pas très évident. Au contraire, le langage du §8.2.1 est à l'origine de mon idée d'utiliser char pour exploiter l'aliasing légal. Et dans mon exemple, il n'y a pas de rétro-conversion de T en char (la manière illégale indiquée par bolov). Si cette méthode est vraiment hors de question, je devrai supprimer ma réponse. Mais ce n'est pas clair que ce soit le cas.


@ v.oddou Vous l'avez à l'envers. La représentation octet de tout objet peut être accédée en convertissant un pointeur vers cet objet en un pointeur vers char (ou unsigned char ou std :: byte ). Le contraire n'est pas vrai. Vous ne pouvez pas transtyper un pointeur vers char vers un type non lié et accéder à un objet via le pointeur avec poinçon de type. char * cp = & someObject; char c = * cp; est valide, SomeType * someObjectPtr = someCharArray; SomeType someObject = * someObjectPtr; est un comportement non défini. Voir [basic.lval] .


@MilesBudnek, allez, voici comment fonctionne aligné_storage ; il fait juste cela. Si c'est UB, alors les goûts de facultatif sont basés sur UB, n'est-ce pas. Ou est-ce que vous me dites que le nouveau placement au milieu le rend soudain pas aliasing?


Le standardese dit «accéder à la valeur stockée d'un objet via un char est valide» J'interprète votre deuxième exemple comme tombant dans ce cas. Il n'est pas clair que la façon dont vous interprétez la représentation char alors obtenue est contrainte par cette formulation.


@ v.oddou C'est la différence entre un objet et une valeur . Vous pouvez accéder à un objet de tout type via une valeur de type char . Mon premier exemple accède à un objet de type SomeType via une valeur de type char (OK). La seconde tente d'accéder à un objet de type char via une valeur de type SomeType (non défini). Ce n'est pas comment fonctionne aligné_storage . Pour utiliser aligné_storage , vous devez construire un nouvel objet dans le stockage précédemment occupé par l'objet de type aligné_storage <> :: type en utilisant un placement- nouvelle expression.


@MilesBudnek merci pour votre aide, mais je pense que nous pouvons nous arrêter là lol :) Je maintiens que construire un nouvel objet dans le stockage ne lève pas comme par magie les restrictions que vous voulez accorder au paragraphe 8.2.1.11 . Parce que new n'est pas une construction de la machine abstraite. Il n'a pas plus de privilèges qu'un accès brut. Et construire un objet, c'est écrire des trucs de type T sur la représentation objet de cet espace, qui devrait être UB par votre interprétation dès que nous interprétons cet espace comme un T * (la valeur de retour de new). Et quand vous programmez une classe variant (je l'ai fait), vous ne pouvez pas ...


... gardez le résultat de retour de new, mais lancez simplement votre stockage sous-jacent lorsque les clients l'interrogent via get () . Et il est difficile de prouver qu'il existe une différence de chemin d'analyse statique par les compilateurs entre les deux approches.


En passant, le paragraphe 6.7.4 mentionne la différence entre la représentation de valeur et la représentation d'objet étant les trous de remplissage potentiels


@ v.oddou Que voulez-vous dire que new n'a plus de privilèges? Il a sa propre section de la norme! [basic.stc.dynamic] dit explicitement < i> new-expressions crée des objets. Oui, vous pouvez lancer un pointeur vers aligné_storage <> :: type . Puisque C ++ 17, cela nécessite un std :: launder , puisque aucun objet de type aligné_storage <> :: type n'existe plus . Sa durée de vie a pris fin lorsque le stockage a été réutilisé.


continuons cette discussion dans le chat .


@MilesBudnek et v.oddou pourrait l'un de vous, s'il vous plaît résumer la conclusion de vos discussions et chat? Je suis un peu perdu dans vos allers-retours, mais j'aimerais inclure des parties pertinentes de votre discussion dans la réponse à la question.


@Lawless je l'ai fait dans les parties EDIT de ma réponse. La discussion mentionne qu'avant C ++ 17, il était une source active de guerres d'interprétation (un peu comme les commentaires ci-dessus. Problème également mentionné dans la réponse d'Alf stackoverflow.com/a/27492569/893406 ) Mais l'esprit que le comité voulait véhiculer a été clarifié en C ++ 17 par la notion de "durée de vie": il est fondamentalement impossible d'interpréter un espace mémoire à travers 2 différents types en même temps, par l'esprit de la nouvelle norme. La seule issue possible pour vous est d'utiliser la solution de Jerry Coffin; ou un memcpy (solution bit_cast ).


Un autre aspect intéressant était que lors de l'analyse de la manière dont la variante est implémentée par gcc, Miles Budnek estime que le double cast statique à travers un vide * et vers T * est un truc énervé, mais peut-être que les implémenteurs de bibliothèques peuvent être du côté d'UB tant qu'ils savent comment gcc est implémenté et ce qu'il fera réellement.


Enfin, ce que je trouve fascinant, c'est l'introduction d'un nouvel engin de langage appelé barrière d'optimisation du pointeur qui est std :: launder . Cette petite installation ne ressemble à rien en cppreference, et je ne manquerai pas d'en lire plus. Mais c'est le même concept que les barrières de prévention de réorganisation de chargement / stockage pour mutex. Et cela semble être un gros problème pour moi, d'autant plus que cela ressemble exactement à la primitive qui rendrait possible tout ce aliasing char to T *, en brisant la confiance du compilateur dans la mise à jour des registres impliqués, et forcer un rechargement.


@ v.oddou J'ai vraiment essayé de comprendre pourquoi le code que vous avez publié ci-dessus est illégal. Malheureusement, je ne suis pas un expert en montage. J'apprécierais que vous me donniez une brève explication.


@Lawless bien illégal ne veut pas dire ne fonctionne pas. J'ai testé ce code dans MSVC et il fait la chose attendue. Ce qui signifie illégal, c'est que nous n'avons aucune garantie qu'une mise à jour du compilateur va préserver ce bon comportement. Ou par exemple, construire avec clang pour tout ce que nous savons. Si vous deviez écrire cette réinterprétation en assembly, vous n'auriez pas un tel problème d'illégalité. Le problème est lié aux optimisations que la compilation en assemblage est autorisée à entreprendre.


@ v.oddou je vois. Mais cela ne rendra-t-il pas reinterpret_cast complètement inutile? Cela ne peut pas être un problème à vie parce que vous utilisez un pointeur vers un jeu de mots.


@Lawless oui avec de telles règles, la réinterprétation semble inutile. C'est un bon point. La durée de vie dans mon exemple commence après new et se termine à la destruction ~ D0 () . Peut-être que tant que vous accédez à inst1 en lecture seule, vous vous en tirerez bien? Je n'ai plus aucune idée honnêtement. Cette langue est incommensurable.


Après l'avoir examiné une deuxième fois, je pense que mon exemple est faux. Je dois permuter la mutation à une chaîne différente, et l'appel au blanchiment. Parce qu'après le blanchissage, le compilateur peut pré-exécuter le chemin qui initialise la chaîne à "stuff" et conserver ce que inst1 pointe comme "stuff". launder force un rechargement donc il doit venir après la mission. Laissez-moi résoudre ce problème.


Ce n'est pas l'accès aux chaînes qui pose problème. L'accès à un Dynamique via un pointeur vers Dynamique entraîne un comportement indéfini. std :: launder ne peut pas vous aider ici, car il n'y a pas d'objet Dynamic à cette adresse pour qu'il renvoie un pointeur. Il n'y a aucun moyen pour cela de "fonctionner", car il n'y a pas de comportement correct défini. Il peut "faire ce que vous voulez", mais ce n'est que par coïncidence, et il peut cesser de faire ce que vous voulez pour n'importe quelle raison à tout moment.


Re reinterpret_cast étant inutile: son utilité est limitée. La principale utilité de reinterpert_cast est d'accéder à la représentation en octets d'un objet en convertissant un pointeur vers celui-ci en un pointeur sur char .



0
votes

Après avoir lu la discussion dans https://stackoverflow.com/a/57318684/2166857 et lu le code source pour bit_cast et beaucoup de recherches en ligne, je pense avoir trouvé la solution la plus sûre à mon problème. Cela ne fonctionne que si

​​1) L'alignement et la taille des deux types correspondent

2) les deux types sont facilement copiables ( https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable )

Commencez par définir un type de mémoire en utilisant aligné_storage p>

{
    Dynamic<true>* dyn_ptr_true=reinterpret_cast<Dynamic<true>*>(&dynamic_buff)
    // do some stuff with dyn_ptr_true
}

puis introduisez la variable de ce type

new (&dynamic_buff) Dynamic<true>();

puis utilisez placement new pour initier l'objet à la classe primaire (dans mon case Dynamic)

DynamicMem dynamic_buff;

puis chaque fois que cela est nécessaire, utilisez reinterpret_cast pour définir la référence d'objet ou le pointeur qui lui est associé dans une portée

typedef std::aligned_storage<sizeof(Dynamic<true>),alignof(Dynamic<true>)>::type DynamicMem;

Cette solution n'est en aucun cas parfaite, mais elle fait le travail pour moi. J'encourage tout le monde à lire le fil de discussion https://stackoverflow.com/a/57318684/2166857 et à suivre le dos et entre @Miles_Budnek et @ v.oddou. J'en ai certainement beaucoup appris.


1 commentaires

Malheureusement, la norme n'a pas spécifié que si un T & dont la cible est accessible en tant que type T est réinterprété-casté en U &, et pendant toute la durée de vie de la référence convertie, l'objet est accédé exclusivement par son intermédiaire, la référence doit être utilisable l'objet en tant que type U même si les types T et U seraient autrement incompatibles. Il n'y a jamais eu de raison pour que les compilateurs non obtus ne prennent pas en charge une telle construction, mais les responsables de clang et gcc utilisent le fait que ce n'est pas une excuse pour ne pas le supporter, et bloquent tout effort pour changer le Standard pour l'exiger .



0
votes

La seule méthode de poinçonnage de type sûre dans le C ++ standard est via std :: bit_cast . Cependant, cela peut impliquer une copie au lieu de traiter la même représentation mémoire comme un type différent si le compilateur n'est pas en mesure de l'optimiser. En outre, std :: bit_cast est actuellement uniquement pris en charge par MSVC a > bien que dans Clang, vous pouvez utiliser __builtin_bit_cast

Puisque vous êtes en utilisant GCC, vous pouvez utiliser l'attribut __may_alias__ pour lui dire que l'aliasing est sûr

template<int T>
struct Dynamic {};

template<>
struct Dynamic<true>
{
    uint32_t v;
} __attribute__((__may_alias__));

template<>
struct Dynamic<false>
{
    float v;
} __attribute__((__may_alias__));

float f(Dynamic<true>& d_true)
{
    auto& d_false = *reinterpret_cast<Dynamic<false>*>(&d_true);
    return d_false.v;
}

Clang et ICC prennent également en charge cet attribut. Voir démo sur Godbolt , aucun avertissement n'est g iven


0 commentaires