Supposons que j'ai une structure comme celle-ci:
struct MyStruct
{
uint8_t var0;
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
};
Cela va peut-être gaspiller un tas (enfin pas une tonne) d'espace. Cela est dû à l'alignement nécessaire du uint32_t
variable.
En réalité (après avoir aligné la structure afin qu'elle puisse réellement utiliser le uint32_t
variable), cela pourrait ressembler à ceci:
struct MyStruct
{
uint8_t var0;
uint8_t unused[3]; //3 bytes of wasted space
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
};
Une structure plus efficace serait:
struct MyStruct
{
uint8_t var0;
uint8_t var2;
uint8_t var3;
uint8_t var4;
uint32_t var1;
};
Maintenant, la question est:
Pourquoi le compilateur est-il interdit (par la norme) de réorganiser la structure?
Je ne vois aucun moyen de vous tirer une balle dans le pied si la structure était réorganisée.
Pourquoi le compilateur est-il interdit (par la norme) de réorganiser la structure?
La raison fondamentale est: pour la compatibilité avec C.
N'oubliez pas que C est, à l'origine, un langage d'assemblage de haut niveau. Il est assez courant en C de visualiser la mémoire (paquets réseau, ...) en réinterprétant les octets comme un struct
spécifique.
Cela a conduit à plusieurs fonctionnalités s'appuyant sur cette propriété:
C garantit que l'adresse d'un struct
et l'adresse de son premier membre de données sont identiques, donc C++ fait de même (en l'absence d'héritage/méthodes virtual
).
C garanti que si vous avez deux struct
A
et B
et que les deux commencent par un membre de données char
suivi d'un membre de données int
(et ce qui suit), puis lorsque vous les placez dans un union
, vous pouvez écrire le membre B
et lire les char
et int
via son A
membre, donc C++ aussi: Disposition standard .
Ce dernier est extrêmement large et empêche complètement tout réordonnancement des membres de données pour la plupart des struct
(ou class
).
Notez que la norme autorise une certaine réorganisation: puisque C n'avait pas le concept de contrôle d'accès, C++ spécifie que l'ordre relatif de deux membres de données avec un spécificateur de contrôle d'accès différent n'est pas spécifié.
Pour autant que je sache, aucun compilateur ne tente d'en tirer parti; mais ils pourraient en théorie.
En dehors de C++, des langages tels que Rust permettent aux compilateurs de réorganiser les champs et le principal Rust compilateur (rustc) le fait par défaut. Seules les décisions historiques et un fort désir de compatibilité descendante empêche C++ de le faire.
Je ne vois aucun moyen de vous tirer une balle dans le pied si la structure était réorganisée.
Vraiment? Si cela était autorisé, la communication entre bibliothèques/modules même dans le même processus serait ridiculement dangereuse par défaut.
Nous devons être capables de savoir que nos structures sont définies de la manière que nous leur avons demandée. C'est déjà assez dommage que le rembourrage ne soit pas spécifié! Heureusement, vous pouvez contrôler cela lorsque vous en avez besoin.
D'accord, théoriquement, un nouveau langage pourrait être créé de telle sorte que, de la même manière, les membres pourraient être réorganisés sauf si un attribut a été donné. Après tout, nous ne sommes pas censés faire de la magie au niveau de la mémoire sur les objets, donc si l'on n'utilisait que des idiomes C++, vous seriez en sécurité par défaut.
Mais ce n'est pas la réalité pratique dans laquelle nous vivons.
Vous pourriez mettre les choses en sécurité si, selon vos mots, "la même commande était utilisée à chaque fois". Le libellé devrait indiquer sans ambiguïté comment les membres seraient classés. C'est compliqué à écrire dans la norme, compliqué à comprendre et compliqué à implémenter.
Il est beaucoup plus facile de simplement garantir que l'ordre sera tel qu'il est dans le code et de laisser ces décisions au programmeur. Rappelez-vous, ces règles ont Origin dans l'ancien C, et l'ancien C donne le pouvoir au programmeur.
Vous avez déjà montré dans votre question à quel point il est facile de rendre le struct padding efficace avec un changement de code trivial. Il n'y a pas besoin de complexité supplémentaire au niveau de la langue pour le faire pour vous.
La norme garantit un ordre d'allocation simplement parce que les structures peuvent représenter une certaine disposition de mémoire, comme un protocole de données ou une collection de registres matériels. Par exemple, ni le programmeur ni le compilateur ne sont libres de réorganiser l'ordre des octets dans le protocole TPC/IP, ou les registres matériels d'un microcontrôleur.
Si l'ordre n'était pas garanti, structs
serait de simples conteneurs de données abstraits (similaires au vecteur C++), dont nous ne pouvons pas supposer grand-chose, sauf qu'ils contiennent en quelque sorte les données que nous y mettons. Cela les rendrait beaucoup plus inutiles lors de toute forme de programmation de bas niveau.
Le compilateur doit conserver l'ordre de ses membres dans le cas où les structures sont lues par tout autre code de bas niveau produit par un autre compilateur ou un autre langage. Supposons que vous créiez un système d'exploitation et que vous décidiez d'en écrire une partie en C et une partie dans Assembly. Vous pouvez définir la structure suivante:
struct keyboard_input
{
uint8_t modifiers;
uint32_t scancode;
}
Vous passez cela à une routine d'assemblage, où vous devez spécifier manuellement la disposition de la mémoire de la structure. Vous vous attendez à pouvoir écrire le code suivant sur un système avec un alignement sur 4 octets.
; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]
Supposons maintenant que le compilateur modifie l'ordre des membres dans la structure d'une manière définie par l'implémentation, cela signifie que selon le compilateur que vous utilisez et les indicateurs que vous lui passez, vous pouvez soit vous retrouver avec le premier octet du scancode membre dans al, ou avec le membre modificateurs.
Bien sûr, le problème n'est pas seulement réduit à des interfaces de bas niveau avec des routines d'assemblage, mais apparaît également si des bibliothèques construites avec différents compilateurs s'appellent (par exemple, la construction d'un programme avec mingw à l'aide de l'API Windows).
Pour cette raison, le langage vous oblige simplement à penser à la disposition de la structure.
N'oubliez pas que non seulement le réordonnancement automatique des éléments pour améliorer le conditionnement peut fonctionner au détriment des dispositions de mémoire spécifiques ou de la sérialisation binaire, mais l'ordre des propriétés peut avoir été soigneusement choisi par le programmeur pour bénéficier de la cache-localité des membres fréquemment utilisés par rapport à le plus rarement accessible.
Le langage conçu par Dennis Ritchie définit la sémantique des structures non pas en termes de comportement, mais en termes de disposition de la mémoire. Si une structure S avait un membre M de type T à l'offset X, le comportement de MS était défini comme prenant l'adresse de S, y ajoutant X octets, l'interprétant comme un pointeur vers T et interprétant le stockage identifié ainsi comme une valeur. L'écriture d'un membre de structure changerait le contenu de son stockage associé, et le changement du contenu du stockage d'un membre changerait la valeur d'un membre. Le code était libre d'utiliser une grande variété de façons de manipuler le stockage associé aux membres de la structure, et la sémantique serait définie en termes d'opérations sur ce stockage.
L'utilisation de memcpy () pour copier une partie arbitraire d'une structure dans une partie correspondante d'une autre, ou memset () pour effacer une partie arbitraire d'une structure, était l'un des moyens utiles par lesquels le code pouvait manipuler le stockage associé à une structure. Étant donné que les membres de la structure étaient disposés séquentiellement, une plage de membres pouvait être copiée ou effacée à l'aide d'un seul appel memcpy () ou memset ().
Le langage défini par le comité standard élimine dans de nombreux cas l'exigence selon laquelle les modifications apportées aux membres de la structure doivent affecter le stockage sous-jacent, ou que les modifications apportées au stockage affectent les valeurs des membres, rendant les garanties sur la disposition de la structure moins utiles qu'elles ne l'étaient dans la langue de Ritchie. Néanmoins, la possibilité d'utiliser memcpy () et memset () a été conservée, et pour conserver cette capacité, il fallait garder les éléments de structure séquentiels.
Vous citez également C++, je vais donc vous donner des raisons pratiques pour lesquelles cela ne peut pas se produire.
Étant donné il n'y a pas de différence entre class
et struct
, considérez:
class MyClass
{
string s;
anotherObject b;
MyClass() : s{"hello"}, b{s}
{}
};
Maintenant, C++ requiert que les membres de données non statiques soient initialisés dans l'ordre dans lequel ils ont été déclarés:
- Ensuite, les membres de données non statiques sont initialisés dans l'ordre dans lequel ils ont été déclarés dans la définition de classe
selon [base.class.init/13
] . Le compilateur ne peut donc pas réorganiser les champs dans la définition de classe, car sinon (à titre d'exemple) les membres dépendant de l'initialisation des autres ne pourraient pas fonctionner.
Le compilateur n'est pas strictement requis, ne les réorganisez pas en mémoire (pour ce que je peux dire) - mais, surtout compte tenu de l'exemple ci-dessus, il serait terriblement douloureux de garder une trace de cela. Et je doute de toute amélioration des performances, contrairement au rembourrage.