Qu'est-ce que le pseudonyme strict et pourquoi devrions-nous nous en préoccuper? Partie 1

(OU dactylographie, comportement vague et alignement, oh mon Dieu!)

Bonjour à tous, dans quelques semaines nous lançons un nouveau fil de discussion au cours "Développeur C ++" . Cet événement sera dédié à notre matériel d'aujourd'hui.

Qu'est-ce qu'un alias strict? Tout d'abord, nous décrivons ce qu'est l'aliasing, puis nous découvrons à quoi sert la rigueur.

En C et C ++, l'aliasing est lié aux types d'expressions auxquelles nous sommes autorisés à accéder aux valeurs stockées. En C et C ++, la norme définit quelles expressions de dénomination sont valides pour quels types. Le compilateur et l'optimiseur sont autorisés à supposer que nous suivons strictement les règles d'alias, d'où le terme est la règle d'alias stricte. Si nous essayons d'accéder à une valeur en utilisant un type non valide, elle est classée comme comportement non défini (UB). Lorsque nous avons un comportement incertain, tous les paris sont faits, les résultats de notre programme cessent d'être fiables.

Malheureusement, avec des violations d'alias strictes, nous obtenons souvent les résultats escomptés, laissant la possibilité qu'une future version du compilateur avec une nouvelle optimisation viole le code que nous considérions comme valide. Cela n'est pas souhaitable, il vaut la peine de comprendre les règles strictes de l'aliasing et d'éviter de les enfreindre.



Afin de mieux comprendre pourquoi nous devrions nous inquiéter à ce sujet, nous discuterons des problèmes qui surviennent lors de la violation des règles strictes d'aliasing, type punning, car il est souvent utilisé dans les règles strictes d'aliasing, ainsi que de la façon de créer correctement un jeu de mots, ainsi que une aide possible avec C ++ 20 pour simplifier le jeu de mots et réduire le risque d'erreurs. Nous résumerons la discussion en considérant quelques méthodes de détection des violations des règles strictes d'alias.

Exemples préliminaires

Jetons un coup d’œil à quelques exemples, puis nous pourrons discuter de ce qui est énoncé exactement dans les normes, examiner quelques exemples supplémentaires, puis voir comment éviter un alias strict et identifier les violations que nous avons manquées. Voici un exemple qui ne devrait pas vous surprendre:

int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n"; 

Nous avons int * pointant vers la mémoire occupée par int, et ceci est un alias valide. L'optimiseur doit supposer que les affectations via ip peuvent mettre à jour la valeur occupée par x.

L' exemple suivant montre l'aliasing, ce qui conduit à un comportement non défini:

 int foo( float *f, int *i ) { *i = 1; *f = 0.f; return *i; } int main() { int x = 0; std::cout << x << "\n"; // Expect 0 x = foo(reinterpret_cast<float*>(&x), &x); std::cout << x << "\n"; // Expect 0? } 

Dans la fonction foo, nous prenons int * et float *. Dans cet exemple, nous appelons foo et définissons les deux paramètres pour pointer vers le même emplacement mémoire, qui dans cet exemple contient un int. Notez que reinterpret_cast indique au compilateur de traiter l'expression comme si elle avait le type spécifié par le paramètre de modèle. Dans ce cas, nous lui demandons de traiter l'expression & x comme si elle était de type float *. Nous pouvons naïvement nous attendre à ce que le résultat du deuxième cout soit 0, mais lorsque l'optimisation est activée en utilisant -O2 et gcc, et clang obtiendra le résultat suivant:
0
1

Ce qui peut être inattendu, mais tout à fait correct, car nous avons causé un comportement indéfini. Float ne peut pas être un alias valide d'un objet int. Par conséquent, l'optimiseur peut supposer que la constante 1 stockée lors du déréférencement i sera la valeur de retour, car l'enregistrement via f ne peut pas affecter correctement l'objet int. La connexion du code dans l'explorateur du compilateur montre que c'est exactement ce qui se passe ( exemple ):

 foo(float*, int*): # @foo(float*, int*) mov dword ptr [rsi], 1 mov dword ptr [rdi], 0 mov eax, 1 ret 

Un optimiseur utilisant l'analyse d'alias basée sur le type (TBAA) suppose que 1 sera renvoyé et déplace directement la valeur constante vers le registre eax, qui stocke la valeur de retour. TBAA utilise des règles de langue sur les types autorisés pour l'alias afin d'optimiser le chargement et le stockage. Dans ce cas, TBAA sait que float ne peut pas être un alias d'int et optimise i loading to death.

Maintenant à la référence

Que dit exactement la norme sur ce que nous sommes autorisés et non autorisés à faire? Le langage standard n'est pas simple, donc pour chaque élément, je vais essayer de fournir des exemples de code qui démontrent le sens.

Que dit la norme C11?

La norme C11 dit ce qui suit dans la section «6.5 Expressions» du paragraphe 7:

L'objet doit avoir sa propre valeur stockée, dont l'accès est effectué uniquement à l'aide de l'expression lvalue, qui a l'un des types suivants: 88) - un type compatible avec le type effectif de l'objet

 int x = 1; int *p = &x; printf("%d\n", *p); //* p   lvalue-  int,    int 

- une version qualifiée d'un type compatible avec le type d'objet actuel,

 int x = 1; const int *p = &x; printf("%d\n", *p); // * p   lvalue-  const int,    int 

- un type qui est un type avec ou sans signe correspondant à un type d'objet qualifié,

 int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  unsigned int,      

Voir la note de bas de page 12 pour l'extension gcc / clang , qui vous permet d'affecter un int * int * non signé, même s'il ne s'agit pas de types compatibles.

- un type qui est un type avec ou sans signe correspondant à une version qualifiée du type d'objet courant,

 int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  const unsigned int,     ,        

- un type agrégé ou combiné qui inclut l'un des types ci-dessus parmi ses membres (y compris, récursivement, un membre d'une association sous-agrégée ou contenue), ou

 struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo -  ,   int   ,       *ip // foo f; foobar( &f, &f.x ); 

- type de caractère.

 int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p   lvalue-  char,    . //    -    . 

Ce que le projet de norme C ++ 17 dit

La norme de projet C ++ 17 dans la section 11 [basic.lval] stipule: si un programme essaie d'accéder à une valeur stockée d'un objet via une valeur gl autre que l'un des types suivants, le comportement n'est pas défini: 63 (11.1) est un type d'objet dynamique,

 void *p = malloc( sizeof(int) ); //   ,       int *ip = new (p) int{0}; // placement new      int std::cout << *ip << "\n"; // * ip   glvalue-  int,       

(11.2) - version qualifiée cv (cv - const et volatile) du type dynamique d'un objet,

 int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip    glvalue  const int,   cv-    x 

(11.3) - un type similaire (tel que défini en 7.5) au type dynamique d'un objet,

//

(11.4) - un type qui est un type avec ou sans signe correspondant au type dynamique d'un objet,
// si ui ,
// godbolt (https://godbolt.org/g/KowGXB) , .

 signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; } 

(11.5) - un type qui est un type avec ou sans signe, correspondant à la version qualifiée cv du type dynamique d'un objet,

 signed int foo( const signed int &si1, int &si2); //  ,     

(11.6) - un type agrégé ou combiné qui inclut l'un des types ci-dessus parmi ses éléments ou éléments de données non statiques (y compris, récursivement, un élément ou un élément de données non statique d'un sous-agrégat ou contenant des associations),

 struct foo { int x; }; 

// Compiler Explorer (https://godbolt.org/g/z2wJTC)

 int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx ); 

(11.7) - un type qui est (éventuellement qualifié cv) un type de classe de base d'un type d'objet dynamique,

 struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; } 

(11.8) - tapez char, unsigned char ou std :: byte.

 int foo( std::byte &b, uint32_t &ui ) { b = static_cast<std::byte>('a'); ui = 0xFFFFFFFF; return std::to_integer<int>( b ); // b   glvalue-  std::byte,      uint32_t } 

Il convient de noter que le caractère signed char pas inclus dans la liste ci-dessus, c'est une différence notable par rapport à C, qui parle du type de caractère.

Différences subtiles

Ainsi, bien que nous puissions voir que C et C ++ disent des choses similaires à propos de l'aliasing, il y a quelques différences dont nous devons être conscients. C ++ n'a pas de concept C d'un type valide ou compatible , et C n'a pas de concept C ++ d'un type dynamique ou similaire. Bien que les deux aient des expressions lvalue et rvalue, C ++ a également des expressions glvalue, prvalue et xvalue. Ces différences sont largement hors de portée de cet article, mais un exemple intéressant est de savoir comment créer un objet à partir de la mémoire utilisée par malloc. En C, nous pouvons définir un type valide, par exemple, écrire dans la mémoire via lvalue ou memcpy.

 //     C,    C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); //   *p - float  C //  float *fp = p; *fp = 1.0f; //   *p - float  C 

Aucune de ces méthodes n'est suffisante en C ++, ce qui nécessite le placement de new:

 float *fp = new (p) float{1.0f} ; //   *p  float 

Les types de caractères int8_t et uint8_t sont-ils?

Théoriquement, ni int8_t ni uint8_t ne devraient être de type char, mais en pratique ils sont implémentés de cette façon. Ceci est important car s'il s'agit vraiment de types de caractères, ce sont également des alias comme les types char. Si vous n'en êtes pas conscient, cela peut entraîner une dégradation inattendue des performances . Nous voyons que glibc typedef int8_t et uint8_t pour signed char et unsigned char .

Ce serait difficile à changer, car pour C ++, ce serait un écart ABI. Cela modifierait la distorsion du nom et briserait n'importe quelle API utilisant l'un de ces types dans leur interface.

La fin de la première partie. Et nous parlerons du jeu de mots de frappe et d'alignement dans quelques jours.

Écrivez vos commentaires et ne manquez pas le webinaire ouvert , qui sera organisé le 6 mars par le responsable du développement technologique chez Rambler & Co, Dmitry Shebordaev .

Source: https://habr.com/ru/post/fr442554/


All Articles