aliasing et alignement ssortingct

J’ai besoin d’un moyen sûr de créer un alias entre des types de POD arbitraires, conformes à ISO-C ++ 11 et prenant explicitement en compte les versions 3.10 / 10 et 3.11 de n3242 ou ultérieur. Il y a beaucoup de questions sur l’aliasing ssortingct ici, la plupart d’entre elles concernant C et non C ++. J’ai trouvé une “solution” pour C qui utilise des unions, en utilisant probablement cette section

type d’union qui inclut l’un des types susmentionnés parmi ses éléments ou des membres de données non statiques

À partir de là j’ai construit ceci.

#include  template  T& access_as(U* p) { union dummy_union { U dummy; T destination; }; dummy_union* u = (dummy_union*)p; return u->destination; } struct test { short s; int i; }; int main() { int buf[2]; static_assert(sizeof(buf) >= sizeof(double), ""); static_assert(sizeof(buf) >= sizeof(test), ""); access_as(buf) = 42.1337; std::cout << access_as(buf) << '\n'; access_as(buf).s = 42; access_as(buf).i = 1234; std::cout << access_as(buf).s << '\n'; std::cout << access_as(buf).i << '\n'; } 

Ma question est la suivante: est-ce que ce programme est légal conformément à la norme? *

Il ne donne aucun avertissement et fonctionne correctement lors de la compilation avec MinGW / GCC 4.6.2 en utilisant:

 g++ -std=c++0x -Wall -Wextra -O3 -fssortingct-aliasing -o alias.exe alias.cpp 

* Edit: Et si non, comment pourrait-on modifier cela pour qu’il soit légal?

Cela ne sera jamais légal, quel que soit le type de contorsion que vous effectuez avec des castes et des unions étranges et ainsi de suite.

Le fait fondamental est le suivant: deux objects de type différent ne peuvent jamais être alias en mémoire, à quelques exceptions près (voir plus bas).

Exemple

Considérons le code suivant:

 void sum(double& out, float* in, int count) { for(int i = 0; i < count; ++i) { out += *in++; } } 

Découpons cela en variables de registre locales pour modéliser l'exécution réelle:

 void sum(double& out, float* in, int count) { for(int i = 0; i < count; ++i) { register double out_val = out; // (1) register double in_val = *in; // (2) register double tmp = out_val + in_val; out = tmp; // (3) in++; } } 

Supposons que (1), (2) et (3) représentent une mémoire en lecture, en lecture et en écriture, respectivement, qui peuvent être des opérations très coûteuses dans une boucle interne aussi serrée. Une optimisation raisonnable pour cette boucle serait la suivante:

 void sum(double& out, float* in, int count) { register double tmp = out; // (1) for(int i = 0; i < count; ++i) { register double in_val = *in; // (2) tmp = tmp + in_val; in++; } out = tmp; // (3) } 

Cette optimisation réduit de moitié le nombre de lectures de mémoire nécessaires et le nombre d'écritures de mémoire à 1. Cela peut avoir un impact considérable sur les performances du code. Il s'agit d'une optimisation très importante pour tous les compilateurs d'optimisation C et C ++.

Maintenant, supposons que nous n'ayons pas de crénelage ssortingct. Supposons qu'une écriture sur un object de n'importe quel type puisse affecter n'importe quel autre object. Supposons que l'écriture d'un double puisse affecter la valeur d'un float quelque part. Cela rend suspecte l'optimisation ci-dessus, car il est possible que le programmeur ait en fait l'intention de créer un pseudonyme de façon à ce que le résultat de la fonction sum soit plus compliqué et affecté par le processus. Cela semble stupide? Même dans ce cas, le compilateur ne peut pas faire la distinction entre un code "stupide" et un code "intelligent". Le compilateur peut uniquement faire la distinction entre un code bien formé et un code mal formé. Si nous autorisons le crénelage libre, le compilateur doit alors conserver ses optimisations de manière prudente et doit effectuer le stockage supplémentaire (3) à chaque itération de la boucle.

Espérons que vous puissiez voir maintenant pourquoi aucune telle astuce syndicale ou de casting ne peut être légale. Vous ne pouvez pas contourner des concepts fondamentaux comme celui-ci par un tour de passe-passe.

Exceptions à l'aliasing ssortingct

Les normes C et C ++ prévoient des dispositions particulières pour le crénelage de tout type avec char , et avec tout "type lié", qui inclut notamment les types dérivés et de base, ainsi que les membres, car il est très important de pouvoir utiliser l'adresse d'un membre du groupe. Vous pouvez trouver une liste exhaustive de ces dispositions dans cette réponse.

En outre, GCC prévoit des dispositions particulières pour la lecture d’un membre d’un syndicat différent de celui qui a été écrit pour la dernière fois. Notez que ce type de conversion par union ne vous permet pas, en fait, de violer le crénelage. Un seul membre d'un syndicat est autorisé à être actif à la fois. Ainsi, même avec GCC, le comportement suivant serait indéfini:

 union { double d; float f[2]; }; f[0] = 3.0f; f[1] = 5.0f; sum(d, f, 2); // UB: attempt to treat two members of // a union as simultaneously active 

Solutions de contournement

Le seul moyen standard de réinterpréter les bits d'un object comme ceux d'un object d'un autre type consiste à utiliser un équivalent de memcpy . Ceci utilise la disposition spéciale pour le crénelage avec des objects char , vous permettant en fait de lire et de modifier la représentation d'object sous-jacente au niveau octet. Par exemple, ce qui suit est légal et ne viole pas les règles ssortingctes en matière de crénelage:

 int a[2]; double d; static_assert(sizeof(a) == sizeof(d)); memcpy(a, &d, sizeof(d)); 

Ceci est sémantiquement équivalent au code suivant:

 int a[2]; double d; static_assert(sizeof(a) == sizeof(d)); for(size_t i = 0; i < sizeof(a); ++i) ((char*)a)[i] = ((char*)&d)[i]; 

GCC prévoit la lecture d’un membre inactif du syndicat, ce qui le rend implicitement actif. De la documentation GCC:

La pratique consistant à lire à partir d'un autre membre du syndicat que celui utilisé le plus récemment (appelé «typage-punning») est courante. Même avec -fssortingct-aliasing, le typage est autorisé, à condition que la mémoire soit accessible via le type d'union. Ainsi, le code ci-dessus fonctionnera comme prévu. Reportez-vous à la section Implémentation des énumérations des unions et des champs de bits Cependant, ce code pourrait ne pas:

 int f() { union a_union t; int* ip; td = 3.0; ip = &t.i; return *ip; } 

De même, l'access en prenant l'adresse, en convertissant le pointeur résultant et en déréférencant le résultat a un comportement indéfini, même si la conversion utilise un type d'union, par exemple:

 int f() { double d = 3.0; return ((union a_union *) &d)->i; } 

Placement nouveau

(Remarque: je vais de mémoire ici car je n'ai pas access à la norme pour le moment). Une fois que vous avez placé un nouvel object dans une mémoire tampon de stockage, la durée de vie des objects de stockage sous-jacents se termine implicitement. Ceci est similaire à ce qui se passe lorsque vous écrivez à un membre d'un syndicat:

 union { int i; float f; } u; // No member of u is active. Neither i nor f refer to an lvalue of any type. ui = 5; // The member ui is now active, and there exists an lvalue (object) // of type int with the value 5. No float object exists. uf = 5.0f; // The member ui is no longer active, // as its lifetime has ended with the assignment. // The member uf is now active, and there exists an lvalue (object) // of type float with the value 5.0f. No int object exists. 

Maintenant, regardons quelque chose de similaire avec placement-new:

 #define MAX_(x, y) ((x) > (y) ? (x) : (y)) // new returns suitably aligned memory char* buffer = new char[MAX_(sizeof(int), sizeof(float))]; // Currently, only char objects exist in the buffer. new (buffer) int(5); // An object of type int has been constructed in the memory pointed to by buffer, // implicitly ending the lifetime of the underlying storage objects. new (buffer) float(5.0f); // An object of type int has been constructed in the memory pointed to by buffer, // implicitly ending the lifetime of the int object that previously occupied the same memory. 

Ce type de fin de vie implicite ne peut se produire que pour les types avec constructeurs et destructeurs sortingviaux, pour des raisons évidentes.

Mis à part l’erreur quand sizeof(T) > sizeof(U) , le problème pourrait être que l’union a un alignement approprié et peut-être plus élevé que U , à cause de T Si vous n’instanciez pas cette union, de sorte que son bloc de mémoire soit aligné (et suffisamment grand!), Puis récupérez le membre avec le type de destination T , elle se cassera dans le pire des cas.

Par exemple, une erreur d’alignement se produit si vous effectuez la dummy_union* style C de U* , où U requirejs un alignement de 4 octets, sur dummy_union* , où dummy_union nécessite un alignement sur 8 octets, car alignof(T) == 8 . Après cela, vous pouvez éventuellement lire le membre d’union de type T aligné sur 4 au lieu de 8 octets.


Alias ​​cast (réinterprète_cast avec alignement et taille correcte pour les POD uniquement):

Cette proposition enfreint explicitement l’aliasing ssortingct, mais avec des assertions statiques:

 ///@brief Comstack time checked reinterpret_cast where destAlign <= srcAlign && destSize <= srcSize template inline _TargetPtrType alias_cast(_ArgType* const ptr) { //assert argument alignment at runtime in debug builds assert(uintptr_t(ptr) % alignof(_ArgType) == 0); typedef typename std::tr1::remove_pointer<_targetptrtype>::type target_type; static_assert(std::tr1::is_pointer<_targetptrtype>::value && std::tr1::is_pod::value, "Target type must be a pointer to POD"); static_assert(std::tr1::is_pod<_argtype>::value, "Argument must point to POD"); static_assert(std::tr1::is_const<_argtype>::value ? std::tr1::is_const::value : true, "const argument must be cast to const target type"); static_assert(alignof(_ArgType) % alignof(target_type) == 0, "Target alignment must be <= source alignment"); static_assert(sizeof(_ArgType) >= sizeof(target_type), "Target size must be <= source size"); //reinterpret cast doesn't remove a const qualifier either return reinterpret_cast<_targetptrtype>(ptr); } 

Utilisation avec un argument de type pointeur (comme les opérateurs de dissortingbution standard tels que reinterpret_cast):

 int* x = alias_cast(any_ptr); 

Une autre approche (contourne les problèmes d’alignement et de crénelage en utilisant une union temporaire):

 template inline ReturnType alias_value(const ArgType& x) { //test argument alignment at runtime in debug builds assert(uintptr_t(&x) % alignof(ArgType) == 0); static_assert(!std::tr1::is_pointer::value ? !std::tr1::is_const::value : true, "Target type can't be a const value type"); static_assert(std::tr1::is_pod::value, "Target type must be POD"); static_assert(std::tr1::is_pod::value, "Argument must be of POD type"); //assure, that we don't read garbage static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size"); union dummy_union { ArgType x; ReturnType r; }; dummy_union dummy; dummy.x = x; return dummy.r; } 

Usage:

 struct characters { char c[5]; }; //..... characters chars; chars.c[0] = 'a'; chars.c[1] = 'b'; chars.c[2] = 'c'; chars.c[3] = 'd'; chars.c[4] = '\0'; int r = alias_value(chars); 

L’inconvénient est que l’union peut nécessiter plus de mémoire que ce qui est réellement nécessaire pour ReturnType.


Memcpy encapsulé (contourne les problèmes d'alignement et de crénelage à l'aide de memcpy):

 template inline ReturnType alias_value(const ArgType& x) { //assert argument alignment at runtime in debug builds assert(uintptr_t(&x) % alignof(ArgType) == 0); static_assert(!std::tr1::is_pointer::value ? !std::tr1::is_const::value : true, "Target type can't be a const value type"); static_assert(std::tr1::is_pod::value, "Target type must be POD"); static_assert(std::tr1::is_pod::value, "Argument must be of POD type"); //assure, that we don't read garbage static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size"); ReturnType r; memcpy(&r,&x,sizeof(ReturnType)); return r; } 

Pour les tableaux de taille dynamic de tout type de POD:

 template ReturnType alias_value(const ElementType* const array,const size_t size) { //assert argument alignment at runtime in debug builds assert(uintptr_t(array) % alignof(ElementType) == 0); static const size_t min_element_count = (sizeof(ReturnType) / sizeof(ElementType)) + (sizeof(ReturnType) % sizeof(ElementType) != 0 ? 1 : 0); static_assert(!std::tr1::is_pointer::value ? !std::tr1::is_const::value : true, "Target type can't be a const value type"); static_assert(std::tr1::is_pod::value, "Target type must be POD"); static_assert(std::tr1::is_pod::value, "Array elements must be of POD type"); //check for minimum element count in array if(size < min_element_count) throw std::invalid_argument("insufficient array size"); ReturnType r; memcpy(&r,array,sizeof(ReturnType)); return r; } 

Des approches plus efficaces peuvent faire des lectures explicites non alignées avec des éléments insortingnsèques, comme ceux de SSE, pour extraire les primitives.


Exemples:

 struct sample_struct { char c[4]; int _aligner; }; int test(void) { const sample_struct constPOD = {}; sample_struct pod = {}; const char* str = "abcd"; const int* constIntPtr = alias_cast(&constPOD); void* voidPtr = alias_value(pod); int intValue = alias_value(str,strlen(str)); return 0; } 

EDITS:

  • Les affirmations visant à assurer uniquement la conversion des POD peuvent être améliorées.
  • Suppression des aides de modèles superflus, n'utilisant plus que les traits tr1
  • Assertions statiques pour la clarification et l'interdiction du type de retour valeur constante (non-pointeur)
  • Assertions d'exécution pour les versions de débogage
  • Ajout de qualificatifs const à certains arguments de fonction
  • Une autre fonction de frappe utilisant memcpy
  • Refactoring
  • Petit exemple

Je pense qu’au niveau le plus fondamental, cela est impossible et enfreint le pseudonyme ssortingct. La seule chose que vous avez accomplie est d’inciter le compilateur à ne pas le remarquer.

Ma question est la suivante: est-ce que ce programme est légal conformément à la norme?

Non. L’alignement peut ne pas être naturel en utilisant le pseudonyme que vous avez fourni. L’union que vous avez écrite ne fait que déplacer le sharepoint l’alias. Cela peut sembler fonctionner, mais ce programme peut échouer lorsque les options du processeur, ABI ou les parameters du compilateur changent.

Et si non, comment pourrait-on modifier cela pour qu’il soit légal?

Créez des variables temporaires naturelles et traitez votre stockage comme un blob de mémoire (entrée et sortie du blob vers / depuis les temporaires) ou utilisez une union qui représente tous vos types (rappelez-vous, un élément actif à la fois ici).