Copie de structures avec des membres non initialisés

29

Est-il valide de copier une structure dont certains membres ne sont pas initialisés?

Je soupçonne que c'est un comportement indéfini, mais si c'est le cas, cela rend très dangereux le fait de laisser des membres non initialisés dans une structure (même si ces membres ne sont jamais utilisés directement). Je me demande donc s'il y a quelque chose dans la norme qui le permet.

Par exemple, est-ce valable?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Tomek Czajka
la source
Je me souviens avoir vu une question similaire il y a quelque temps, mais je ne la trouve pas. Cette question est liée comme celle-ci .
1201ProgramAlarm

Réponses:

23

Oui, si le membre non initialisé n'est pas un type de caractère étroit non signé ou std::byte, alors la copie d'une structure contenant cette valeur indéterminée avec le constructeur de copie défini implicitement est un comportement techniquement indéfini, comme pour la copie d'une variable avec une valeur indéterminée du même type, car de [dcl.init] / 12 .

Cela s'applique ici, car le constructeur de copie généré implicitement est, à l'exception de unions, défini pour copier chaque membre individuellement comme si par initialisation directe, voir [class.copy.ctor] / 4 .

Cela fait également l'objet du numéro CWG 2264 actif .

Je suppose qu'en pratique, vous n'aurez aucun problème avec cela.

Si vous voulez être sûr à 100%, l'utilisation a std::memcpytoujours un comportement bien défini si le type est trivialement copiable , même si les membres ont une valeur indéterminée.


Ces problèmes mis à part, vous devez toujours initialiser correctement vos membres de classe avec une valeur spécifiée lors de la construction, en supposant que vous n'avez pas besoin que la classe ait un constructeur par défaut trivial . Vous pouvez le faire facilement en utilisant la syntaxe d'initialisation des membres par défaut pour par exemple initialiser les membres par valeur:

struct Data {
  int a{}, b{};
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
noyer
la source
bien .. cette structure n'est pas un POD (Plain old data)? Cela signifie que les membres seront initialisés avec des valeurs par défaut? C'est un doute
Kevin Kouketsu
N'est-ce pas la copie superficielle dans ce cas? qu'est-ce qui peut mal tourner avec cela à moins qu'un membre non initialisé ne soit accédé dans la structure copiée?
TruthSeeker
@KevinKouketsu J'ai ajouté une condition pour le cas où un type trivial / POD est requis.
noyer
@TruthSeeker La norme indique qu'il s'agit d'un comportement non défini. La raison pour laquelle il s'agit généralement d'un comportement non défini pour les variables (non membres) est expliquée dans la réponse d'AndreySemashev. Fondamentalement, il s'agit de prendre en charge les représentations d'interruption avec de la mémoire non initialisée. Que cela vise à appliquer à la construction de copie implicite de struct est la question de la question liée CWG.
noyer
@TruthSeeker Le constructeur de copie implicite est défini pour copier chaque membre individuellement comme par initialisation directe. Il n'est pas défini pour copier la représentation d'objet comme si par memcpy, même pour les types trivialement copiables. La seule exception concerne les unions, pour lesquelles le constructeur de copie implicite copie la représentation d'objet comme si par memcpy.
noyer
11

En général, la copie de données non initialisées est un comportement non défini car ces données peuvent être dans un état de recouvrement. Citant cette page:

Si une représentation d'objet ne représente aucune valeur du type d'objet, elle est connue sous le nom de représentation d'interruption. L'accès à une représentation d'interruption d'une manière autre que sa lecture via une expression lvalue de type caractère est un comportement non défini.

Les NaN de signalisation sont possibles pour les types à virgule flottante, et sur certaines plates-formes, les entiers peuvent avoir des représentations d' interruption .

Cependant, pour les types trivialement copiables, il est possible d'utiliser memcpypour copier la représentation brute de l'objet. Cette opération est sûre car la valeur de l'objet n'est pas interprétée et la séquence d'octets bruts de la représentation de l'objet est copiée.

Andrey Semashev
la source
Qu'en est-il des données de types pour lesquels tous les modèles de bits représentent des valeurs valides (par exemple, une structure de 64 octets contenant un unsigned char[64])? Traiter les octets d'une structure comme ayant des valeurs non spécifiées pourrait entraver inutilement l'optimisation, mais obliger les programmeurs à remplir manuellement le tableau avec des valeurs inutiles entraverait encore plus l'efficacité.
supercat
L'initialisation des données n'est pas inutile, elle empêche UB, qu'elle soit causée par des représentations d'interruptions ou par l'utilisation de données non initialisées ultérieurement. La remise à zéro de 64 octets (1 ou 2 lignes de cache) n'est pas aussi chère que cela puisse paraître. Et si vous avez de grandes structures où c'est cher, vous devriez réfléchir à deux fois avant de les copier. Et je suis presque sûr que vous devrez les initialiser de toute façon à un moment donné.
Andrey Semashev
Les opérations de code machine qui ne peuvent pas affecter le comportement d'un programme sont inutiles. L'idée selon laquelle toute action qualifiée d'UB par la norme doit être évitée à tout prix, au lieu de dire que [selon les termes du Comité des normes C] UB "identifie les domaines d'extension linguistique possible", est relativement récente. Bien que je n'aie pas vu de justification publiée pour la norme C ++, elle renonce expressément à la juridiction sur ce que les programmes C ++ sont "autorisés" à faire en refusant de classer les programmes comme conformes ou non conformes, ce qui signifie qu'il autoriserait des extensions similaires.
supercat
-1

Dans certains cas, comme celui décrit, la norme C ++ permet aux compilateurs de traiter les constructions de la manière que leurs clients trouveraient la plus utile, sans exiger que ce comportement soit prévisible. En d'autres termes, ces constructions invoquent un "comportement indéfini". Cela n'implique pas, cependant, que de telles constructions sont censées être "interdites" puisque la norme C ++ renonce explicitement à la juridiction sur ce que les programmes bien formés sont "autorisés" à faire. Bien que je ne sois au courant d'aucun document de justification publié pour la norme C ++, le fait qu'il décrit un comportement indéfini, tout comme le fait C89, suggère que la signification voulue est similaire: diagnostiquer.

Il existe de nombreuses situations où le moyen le plus efficace de traiter quelque chose impliquerait d'écrire les parties d'une structure dont le code en aval va se soucier, tout en omettant celles dont le code en aval ne se souciera pas. Exiger que les programmes initialisent tous les membres d'une structure, y compris ceux dont personne ne se souciera jamais, entraverait inutilement l'efficacité.

De plus, dans certaines situations, il peut être plus efficace de faire en sorte que les données non initialisées se comportent de manière non déterministe. Par exemple, étant donné:

struct q { unsigned char dat[256]; } x,y;

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;
}

si le code en aval ne se soucie pas des valeurs des éléments de x.datou y.datdont les indices ne sont pas répertoriés dans arr, le code peut être optimisé pour:

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
  {
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  }
}

Cette amélioration de l'efficacité ne serait pas possible si les programmeurs étaient tenus d'écrire explicitement tous les éléments de temp.dat, y compris ceux en aval qui ne se soucient pas, avant de le copier.

D'un autre côté, il existe certaines applications où il est important d'éviter la possibilité de fuite de données. Dans de telles applications, il peut être utile d'avoir une version du code qui est instrumentée pour intercepter toute tentative de copier le stockage non initialisé sans se soucier de savoir si le code en aval le regarderait, ou il pourrait être utile d'avoir une garantie d'implémentation que tout stockage dont le contenu pourrait être divulgué serait mis à zéro ou remplacé par des données non confidentielles.

D'après ce que je peux dire, le standard C ++ ne tente pas de dire que l'un de ces comportements est suffisamment plus utile que l'autre pour justifier son mandat. Ironiquement, ce manque de spécification peut être destiné à faciliter l'optimisation, mais si les programmeurs ne peuvent exploiter aucune sorte de garanties comportementales faibles, toutes les optimisations seront annulées.

supercat
la source
-2

Étant donné que tous les membres de Datasont de types primitifs, data2obtiendra une "copie bit par bit" exacte de tous les membres de data. La valeur de data2.bsera donc exactement la même que la valeur de data.b. Cependant, la valeur exacte de data.bne peut pas être prédite, car vous ne l'avez pas initialisée explicitement. Cela dépendra des valeurs des octets dans la région de mémoire allouée pour le data.

ivan.ukr
la source
Pouvez-vous soutenir cela avec une référence à la norme? Les liens fournis par @walnut impliquent qu'il s'agit d'un comportement non défini. Existe-t-il une exception pour les POD dans la norme?
Tomek Czajka
Bien que ce qui suit ne soit pas lié à la norme, quand même: en.cppreference.com/w/cpp/language/… "Les objets TriviallyCopyable peuvent être copiés en copiant leurs représentations d'objets manuellement, par exemple avec std :: memmove. Tous les types de données compatibles avec le C langue (types POD) sont copiables. "
ivan.ukr
Le seul "comportement non défini" dans ce cas est que nous ne pouvons pas prédire la valeur de la variable membre non initialisée. Mais le code se compile et s'exécute correctement.
ivan.ukr
1
Le fragment que vous citez parle du comportement de memmove, mais ce n'est pas vraiment pertinent ici parce que dans mon code j'utilise le constructeur de copie, pas memmove. Les autres réponses impliquent que l'utilisation du constructeur de copie entraîne un comportement indéfini. Je pense que vous avez également mal compris le terme "comportement indéfini". Cela signifie que le langage ne fournit aucune garantie, par exemple le programme peut planter ou corrompre des données au hasard ou faire quoi que ce soit. Cela ne signifie pas seulement qu'une valeur est imprévisible, ce serait un comportement non spécifié.
Tomek Czajka
@ ivan.ukr La norme C ++ spécifie que les constructeurs de copie / déplacement implicites agissent par membre comme si par initialisation directe, voir les liens dans ma réponse. Par conséquent, la construction de la copie ne fait pas de " " copie bit à bit " ". Vous n'êtes correct que pour les types d'union, pour lesquels le constructeur de copie implicite est spécifié pour copier la représentation d'objet comme par un manuel std::memcpy. Rien de tout cela n'empêche d'utiliser std::memcpyou std::memmove. Il empêche uniquement l'utilisation du constructeur de copie implicite.
noyer