web-dev-qa-db-fra.com

Pourquoi le comportement de std :: memcpy ne serait-il pas défini pour les objets qui ne sont pas TriviallyCopyable?

De http://en.cppreference.com/w/cpp/string/byte/memcpy :

Si les objets ne sont pas TriviallyCopyable (par exemple, scalaires, tableaux, structures compatibles C), le comportement n'est pas défini.

Dans mon travail, nous avons utilisé std::memcpy depuis longtemps pour permuter au niveau du bit des objets qui ne sont pas TriviallyCopyable en utilisant:

void swapMemory(Entity* ePtr1, Entity* ePtr2)
{
   static const int size = sizeof(Entity); 
   char swapBuffer[size];

   memcpy(swapBuffer, ePtr1, size);
   memcpy(ePtr1, ePtr2, size);
   memcpy(ePtr2, swapBuffer, size);
}

et n'a jamais eu de problèmes.

Je comprends qu'il est trivial d'abuser std::memcpy avec des objets non TriviallyCopyable et provoque un comportement indéfini en aval. Cependant, ma question:

Pourquoi le comportement de std::memcpy lui-même n'est pas défini lorsqu'il est utilisé avec des objets non TriviallyCopyable? Pourquoi la norme juge-t-elle nécessaire de préciser cela?

[~ # ~] mise à jour [~ # ~]

Le contenu de http://en.cppreference.com/w/cpp/string/byte/memcpy a été modifié en réponse à ce message et aux réponses au message. La description actuelle dit:

Si les objets ne sont pas TriviallyCopyable (par exemple, scalaires, tableaux, structures compatibles C), le comportement est indéfini sauf si le programme ne dépend pas des effets du destructeur de l'objet cible (qui n'est pas exécuté par memcpy) et la durée de vie de l'objet cible (qui est terminée mais non démarrée par memcpy) est démarrée par d'autres moyens, tels que placement-new.

[~ # ~] ps [~ # ~]

Commentaire de @Cubbi:

@RSahu si quelque chose garantit UB en aval, cela rend l'ensemble du programme indéfini. Mais je conviens qu'il semble possible de contourner UB dans ce cas et de modifier la préférence en conséquence.

70
R Sahu

Pourquoi le comportement de std::memcpy lui-même n'est pas défini lorsqu'il est utilisé avec des objets non TriviallyCopyable?

Ce n'est pas! Cependant, une fois que vous avez copié les octets sous-jacents d'un objet d'un type non copiable dans un autre objet de ce type, l'objet cible n'est pas vivant. Nous l'avons détruit en réutilisant son stockage, et nous ne l'avons pas revitalisé par un appel de constructeur.

L'utilisation de l'objet cible - appeler ses fonctions membres, accéder à ses membres de données - n'est clairement pas définie[basic.life]/6, tout comme un appel destructeur implicite ultérieur[basic.life]/4 pour les objets cibles ayant une durée de stockage automatique. Notez comment le comportement indéfini est rétrospectif. [intro.execution]/5:

Cependant, si une telle exécution contient une opération non définie, la présente Norme internationale n'impose aucune exigence à l'implémentation exécutant ce programme avec cette entrée (même pas en ce qui concerne les opérations précédant la première opération non définie).

Si une implémentation détecte comment un objet est mort et nécessairement soumis à d'autres opérations non définies, ... il peut réagir en modifiant la sémantique de vos programmes. À partir de l'appel memcpy. Et cette considération devient très pratique une fois que nous pensons aux optimiseurs et à certaines hypothèses qu'ils font.

Il convient de noter que les bibliothèques standard sont capables et autorisées d'optimiser certains algorithmes de bibliothèque standard pour les types trivialement copiables. std::copy sur les pointeurs vers des types trivialement copiables appelle généralement memcpy sur les octets sous-jacents. swap aussi.
. . En outre, cela évite de blesser votre cerveau en vous souciant de parties contradictoires et sous-spécifiées de la langue.

38
Columbo

Il est assez facile de construire une classe où ce memcpy basé sur swap casse:

struct X {
    int x;
    int* px; // invariant: always points to x
    X() : x(), px(&x) {}
    X(X const& b) : x(b.x), px(&x) {}
    X& operator=(X const& b) { x = b.x; return *this; }
};

memcpying un tel objet casse cet invariant.

GNU C++ 11 std::string fait exactement cela avec des chaînes courtes.

Ceci est similaire à la façon dont les flux de fichiers et de chaînes standard sont implémentés. Les flux dérivent finalement de std::basic_ios qui contient un pointeur sur std::basic_streambuf. Les flux contiennent également le tampon spécifique en tant que membre (ou sous-objet de classe de base), vers lequel ce pointeur dans std::basic_ios pointe vers.

23
Maxim Egorushkin

Parce que la norme le dit.

Les compilateurs peuvent supposer que les types non TriviallyCopyable sont copiés uniquement via leurs constructeurs de copie/déplacement/opérateurs d'affectation. Cela peut être à des fins d'optimisation (si certaines données sont privées, il peut différer leur définition jusqu'à ce qu'une copie/un déplacement se produise).

Le compilateur est même libre de prendre votre appel memcpy et de le faire ne rien faire, ou de formater votre disque dur. Pourquoi? Parce que la norme le dit. Et ne rien faire est définitivement plus rapide que de déplacer des bits, alors pourquoi ne pas optimiser votre memcpy en un programme plus rapide tout aussi valide?

Maintenant, dans la pratique, de nombreux problèmes peuvent survenir lorsque vous blit autour de bits dans des types qui ne s'y attendent pas. Les tables de fonctions virtuelles peuvent ne pas être configurées correctement. L'instrumentation utilisée pour détecter les fuites peut ne pas être correctement configurée. Les objets dont l'identité inclut leur emplacement sont complètement gâchés par votre code.

La partie vraiment amusante est que using std::swap; swap(*ePtr1, *ePtr2); devrait pouvoir être compilé vers un memcpy pour les types trivialement copiables par le compilateur, et pour les autres types, un comportement défini. Si le compilateur peut prouver que la copie n'est que des bits copiés, il est libre de le changer en memcpy. Et si vous pouvez écrire un swap plus optimal, vous pouvez le faire dans l'espace de noms de l'objet en question.

22

C++ ne garantit pas pour tous les types que leurs objets occupent des octets de stockage contigus [intro.object]/5

Un objet de type trivialement copiable ou à disposition standard (3.9) doit occuper des octets de stockage contigus.

Et en effet, grâce aux classes de base virtuelles, vous pouvez créer des objets non contigus dans les principales implémentations. J'ai essayé de construire un exemple où se trouve un sous-objet de classe de base d'un objet x avant l'adresse de début de x . Pour visualiser cela, considérez le graphique/tableau suivant, où l'axe horizontal est l'espace d'adressage et l'axe vertical est le niveau d'héritage (le niveau 1 hérite du niveau 0). Les champs marqués par dm sont occupés par membres directs des données de la classe.

 L | 00 08 16 
 - + --------- 
 1 | dm 
 0 | dm 

Il s'agit d'une disposition de mémoire habituelle lors de l'utilisation de l'héritage. Cependant, l'emplacement d'un sous-objet de classe de base virtuelle n'est pas fixe, car il peut être déplacé par des classes enfants qui héritent également de la même classe de base virtuellement. Cela peut conduire à la situation que l'objet de niveau 1 (sous-classe de base) signale qu'il commence à l'adresse 8 et fait 16 octets. Si nous ajoutons naïvement ces deux nombres, nous penserions qu'il occupe l'espace d'adressage [8, 24) même s'il occupe réellement [0, 16).

Si nous pouvons créer un tel objet de niveau 1, nous ne pouvons pas utiliser memcpy pour le copier: memcpy accèderait à la mémoire qui n'appartient pas à cet objet (adresses 16 à 24). Dans ma démo, est détecté comme un débordement de tampon de pile par l'assainisseur d'adresse de clang ++.

Comment construire un tel objet? En utilisant l'héritage virtuel multiple, j'ai trouvé un objet qui a la disposition de mémoire suivante (les pointeurs de table virtuelle sont marqués comme vp). Il est composé de quatre couches d'héritage:

 L 00 08 16 24 32 40 48 
 3 dm 
 2 vp dm 
 1 vp dm 
 0 dm 

Le problème décrit ci-dessus se posera pour le sous-objet de classe de base de niveau 1. Son adresse de départ est 32 et sa taille est de 24 octets (vptr, ses propres membres de données et membres de données de niveau 0).

Voici le code d'une telle disposition de mémoire sous clang ++ et g ++ @ coliru:

struct l0 {
    std::int64_t dummy;
};

struct l1 : virtual l0 {
    std::int64_t dummy;
};

struct l2 : virtual l0, virtual l1 {
    std::int64_t dummy;
};

struct l3 : l2, virtual l1 {
    std::int64_t dummy;
};

Nous pouvons produire un débordement de tampon de pile comme suit:

l3  o;
l1& so = o;

l1 t;
std::memcpy(&t, &so, sizeof(t));

Voici une démo complète qui imprime également quelques informations sur la disposition de la mémoire:

#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>

#define PRINT_LOCATION() \
    std::cout << std::setw(22) << __PRETTY_FUNCTION__                   \
      << " at offset " << std::setw(2)                                  \
        << (reinterpret_cast<char const*>(this) - addr)                 \
      << " ; data is at offset " << std::setw(2)                        \
        << (reinterpret_cast<char const*>(&dummy) - addr)               \
      << " ; naively to offset "                                        \
        << (reinterpret_cast<char const*>(this) - addr + sizeof(*this)) \
      << "\n"

struct l0 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); }
};

struct l1 : virtual l0 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l0::report(addr); }
};

struct l2 : virtual l0, virtual l1 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l1::report(addr); }
};

struct l3 : l2, virtual l1 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l2::report(addr); }
};

void print_range(void const* b, std::size_t sz)
{
    std::cout << "[" << (void const*)b << ", "
              << (void*)(reinterpret_cast<char const*>(b) + sz) << ")";
}

void my_memcpy(void* dst, void const* src, std::size_t sz)
{
    std::cout << "copying from ";
    print_range(src, sz);
    std::cout << " to ";
    print_range(dst, sz);
    std::cout << "\n";
}

int main()
{
    l3 o{};
    o.report(reinterpret_cast<char const*>(&o));

    std::cout << "the complete object occupies ";
    print_range(&o, sizeof(o));
    std::cout << "\n";

    l1& so = o;
    l1 t;
    my_memcpy(&t, &so, sizeof(t));
}

Démo en direct

Exemple de sortie (abrégé pour éviter le défilement vertical):

l3 :: rapport au décalage 0; les données sont à l'offset 16; naïvement pour compenser 48
 l2 :: rapport au décalage 0; les données sont à l'offset 8; naïvement pour compenser 40 
 l1 :: signaler à la compensation 32; les données sont à l'offset 40; naïvement pour compenser 56
 l0 :: rapport à l'offset 24; les données sont à l'offset 24; naïvement pour compenser 32 
 l'objet complet occupe [0x9f0, 0xa20) 
 en copiant de [0xa10, 0xa28) vers [0xa20, 0xa38) 

Notez les deux décalages d'extrémité accentués.

15
dyp

Beaucoup de ces réponses mentionnent que memcpy pourrait casser des invariants dans la classe, ce qui provoquerait un comportement indéfini plus tard (et qui dans la plupart des cas devrait être une raison suffisante pour ne pas le risquer), mais cela ne semble pas être ce que vous demandez vraiment.

L'une des raisons pour lesquelles l'appel memcpy lui-même est considéré comme un comportement non défini est de donner autant de place que possible au compilateur pour effectuer des optimisations basées sur la plate-forme cible. En faisant l'appel lui-même être UB, le compilateur est autorisé pour faire des choses étranges, dépendantes de la plateforme.

Considérez cet exemple (très artificiel et hypothétique): pour une plate-forme matérielle particulière, il peut y avoir plusieurs types de mémoire, certains étant plus rapides que d'autres pour différentes opérations. Il peut y avoir, par exemple, une sorte de mémoire spéciale qui permet des copies de mémoire ultra rapides. Un compilateur pour cette plate-forme (imaginaire) est donc autorisé à placer tous les types TriviallyCopyable dans cette mémoire spéciale et à implémenter memcpy pour utiliser des instructions matérielles spéciales qui ne fonctionnent que sur cette mémoire.

Si vous deviez utiliser memcpy sur des objets nonTriviallyCopyable sur cette plate-forme, il pourrait y avoir un crash OPCODE INVALIDE de bas niveau dans l'appel memcpy lui-même.

Pas l'argument le plus convaincant, peut-être, mais le fait est que le standard ne l'interdit pas, ce qui n'est possible qu'en faisant le memcpycall UB.

5
CAdaker

memcpy copiera tous les octets, ou dans votre cas, permuterez tous les octets, très bien. Un compilateur trop zélé pourrait prendre le "comportement indéfini" comme excuse pour toutes sortes de méfaits, mais la plupart des compilateurs ne le feront pas. Pourtant, c'est possible.

Toutefois, une fois ces octets copiés, l'objet dans lequel vous les avez copiés peut ne plus être un objet valide. La casse simple est une implémentation de chaîne dans laquelle de grandes chaînes allouent de la mémoire, mais de petites chaînes utilisent simplement une partie de l'objet chaîne pour contenir des caractères et gardent un pointeur vers cela. Le pointeur pointera évidemment vers l'autre objet, donc les choses vont mal. Un autre exemple que j'ai vu est une classe avec des données qui ont été utilisées dans très peu de cas seulement, de sorte que les données ont été conservées dans une base de données avec l'adresse de l'objet comme clé.

Maintenant, si vos instances contiennent un mutex par exemple, je pense que le déplacer pourrait être un problème majeur.

3
gnasher729

Tout d'abord, notez qu'il est incontestable que toute la mémoire des objets C/C++ mutables doit être non typée, non spécialisée, utilisable pour tout objet mutable. (Je suppose que la mémoire des variables const globales pourrait être hypothétiquement tapée, il n'y a tout simplement pas de point avec une telle hyper complication pour un si petit cas de coin.) Contrairement à Java, C++ n'a pas d'allocation typée d'un objet dynamique: new Class(args) in Java est une création d'objet typé: création d'un objet d'un type bien défini, qui pourrait vivre dans la mémoire typée. Par contre, l'expression C++ new Class(args) est juste un wrapper de typage fin autour de l'allocation de mémoire sans type, équivalent à new (operator new(sizeof(Class)) Class(args): l'objet est créé en "mémoire neutre". Changer cela signifierait changer une très grande partie de C++.

Interdire l'opération de copie de bits (qu'elle soit effectuée par memcpy ou par la copie octet par octet définie par l'utilisateur équivalent) sur certains types donne beaucoup de liberté à l'implémentation pour les classes polymorphes (celles avec des fonctions virtuelles), et d'autres soi-disant " classes virtuelles "(pas un terme standard), c'est-à-dire les classes qui utilisent le mot clé virtual.

La mise en œuvre de classes polymorphes pourrait utiliser une carte associative globale d'adresses qui associent l'adresse d'un objet polymorphe et ses fonctions virtuelles. Je crois que c'était une option sérieusement envisagée lors de la conception des premières itérations du langage C++ (ou même "C avec classes"). Cette carte d'objets polymorphes peut utiliser des fonctionnalités CPU spéciales et une mémoire associative spéciale (ces fonctionnalités ne sont pas exposées à l'utilisateur C++).

Bien sûr, nous savons que toutes les implémentations pratiques des fonctions virtuelles utilisent vtables (un enregistrement constant décrivant tous les aspects dynamiques d'une classe) et placent un vptr (pointeur vtable) dans chaque sous-objet de classe de base polymorphe, car cette approche est extrêmement simple à implémenter (à moins pour les cas les plus simples) et très efficace. Il n'y a pas de registre global d'objets polymorphes dans aucune implémentation du monde réel, sauf éventuellement en mode débogage (je ne connais pas ce mode de débogage).

La norme C++ a rendu le manque de registre global quelque peu officiel en disant que vous pouvez ignorer l'appel du destructeur lorsque vous réutilisez la mémoire d'un objet, tant que vous ne dépendez pas des "effets secondaires" de cet appel destructeur. (Je crois que cela signifie que les "effets secondaires" sont créés par l'utilisateur, c'est-à-dire le corps du destructeur, et non l'implémentation créée, comme cela est automatiquement fait au destructeur par l'implémentation.)

Parce que dans la pratique dans toutes les implémentations, le compilateur utilise simplement les membres cachés vptr (pointeur vers vtables), et ces membres cachés seront copiés correctement par memcpy; comme si vous aviez fait une copie simple au niveau des membres de la structure C représentant la classe polymorphe (avec tous ses membres cachés). Les copies au niveau du bit ou les copies complètes des membres de la structure C (la structure C complète comprend les membres cachés) se comporteront exactement comme un appel de constructeur (comme cela est fait par placement new), donc tout ce que vous avez à faire laisse le compilateur penser que vous pourriez ont appelé placement nouveau. Si vous effectuez un appel de fonction fortement externe (un appel à une fonction qui ne peut pas être insérée et dont l'implémentation ne peut pas être examinée par le compilateur, comme un appel à une fonction définie dans une unité de code chargée dynamiquement ou un appel système), alors le le compilateur supposera simplement que ces constructeurs auraient pu être appelés par le code qu'il ne peut pas examiner. Ainsi, le comportement de memcpy ici n'est pas défini par la norme de langage, mais par le compilateur ABI (Application Binary Interface). Le comportement d'un appel de fonction fortement externe est défini par l'ABI, et pas seulement par la norme de langage. Un appel à une fonction potentiellement inlinable est défini par le langage car sa définition peut être vue (soit pendant le compilateur, soit pendant l'optimisation globale du temps de liaison).

Donc en pratique, étant donné les "barrières de compilation" appropriées (comme un appel à une fonction externe, ou simplement asm("")), vous pouvez memcpy classes qui utilisent uniquement des fonctions virtuelles.

Bien sûr, vous devez être autorisé par la sémantique du langage à faire un tel placement nouveau lorsque vous faites un memcpy: vous ne pouvez pas redéfinir à volonté le type dynamique d'un objet existant et prétendre que vous n'avez pas simplement détruit l'ancien objet. Si vous avez un sous-objet membre global, statique, automatique, non const, un sous-objet tableau, vous pouvez l'écraser et y placer un autre objet sans rapport; mais si le type dynamique est différent, vous ne pouvez pas prétendre qu'il s'agit toujours du même objet ou sous-objet:

struct A { virtual void f(); };
struct B : A { };

void test() {
  A a;
  if (sizeof(A) != sizeof(B)) return;
  new (&a) B; // OK (assuming alignement is OK)
  a.f(); // undefined
}

Le changement de type polymorphe d'un objet existant n'est tout simplement pas autorisé: le nouvel objet n'a aucune relation avec a sauf pour la région de la mémoire: les octets continus commençant à &a. Ils ont différents types.

[La norme est fortement divisée sur la question de savoir si *&a peut être utilisé (dans les machines à mémoire plates typiques) ou (A&)(char&)a (dans tous les cas) pour faire référence au nouvel objet. Les rédacteurs du compilateur ne sont pas divisés: vous ne devriez pas le faire. C'est un profond défaut en C++, peut-être le plus profond et le plus troublant.]

Mais vous ne pouvez pas en code portable effectuer une copie au niveau du bit de classes qui utilisent l'héritage virtuel, car certaines implémentations implémentent ces classes avec des pointeurs vers les sous-objets de base virtuels: ces pointeurs correctement initialisés par le constructeur de l'objet le plus dérivé verraient leur valeur copiée par memcpy (comme une copie simple des membres de la structure C représentant la classe avec tous ses membres cachés) et ne pointerait pas le sous-objet de l'objet dérivé!

D'autres ABI utilisent des décalages d'adresse pour localiser ces sous-objets de base; ils dépendent uniquement du type de l'objet le plus dérivé, comme les substitutions finales et typeid, et peuvent donc être stockés dans la vtable. Sur ces implémentations, memcpy fonctionnera comme garanti par l'ABI (avec la limitation ci-dessus pour changer le type d'un objet existant).

Dans les deux cas, il s'agit entièrement d'un problème de représentation d'objets, c'est-à-dire d'un problème ABI.

1
curiousguy

Une autre raison pour laquelle memcpy est UB (à part ce qui a été mentionné dans les autres réponses - cela pourrait casser les invariants plus tard) est qu'il est très difficile pour la norme de dire exactement ce qui se passerait.

Pour les types non triviaux, la norme ne dit pas grand-chose sur la façon dont l'objet est disposé en mémoire, dans quel ordre les membres sont placés, où se trouve le pointeur vtable, quel devrait être le remplissage, etc. Le compilateur a d'énormes quantités de liberté en décidant cela.

Par conséquent, même si la norme voulait autoriser memcpy dans ces situations "sûres", il serait impossible d'indiquer quelles situations sont sûres et lesquelles ne le sont pas, ou quand exactement l'UB réel serait déclenché pour cas dangereux.

Je suppose que vous pourriez faire valoir que les effets devraient être définis par l'implémentation ou non spécifiés, mais je pense personnellement que cela creuserait un peu trop profondément dans les spécificités de la plate-forme et donnerait un peu trop de légitimité à quelque chose qui, dans le cas général est plutôt dangereux.

1
CAdaker

Ok, essayons votre code avec un petit exemple:

#include <iostream>
#include <string>
#include <string.h>

void swapMemory(std::string* ePtr1, std::string* ePtr2) {
   static const int size = sizeof(*ePtr1);
   char swapBuffer[size];

   memcpy(swapBuffer, ePtr1, size);
   memcpy(ePtr1, ePtr2, size);
   memcpy(ePtr2, swapBuffer, size);
}

int main() {
  std::string foo = "foo", bar = "bar";
  std::cout << "foo = " << foo << ", bar = " << bar << std::endl;
  swapMemory(&foo, &bar);
  std::cout << "foo = " << foo << ", bar = " << bar << std::endl;
  return 0;
}

Sur ma machine, cela imprime ce qui suit avant de planter:

foo = foo, bar = bar
foo = foo, bar = bar

Bizarre, hein? Le swap ne semble pas du tout effectué. Eh bien, la mémoire a été échangée, mais std::string Utilise l'optimisation des petites chaînes sur ma machine: il stocke des chaînes courtes dans un tampon qui fait partie de l'objet std::string Lui-même, et pointe simplement son interne pointeur de données sur ce tampon.

Lorsque swapMemory() échange les octets, il échange à la fois les pointeurs et les tampons. Ainsi, le pointeur dans l'objet foo pointe désormais vers le stockage dans l'objet bar, qui contient désormais la chaîne "foo". Deux niveaux de swap ne font aucun swap.

Lorsque le destructeur de std::string Essaie par la suite de nettoyer, plus de mal se produit: le pointeur de données ne pointe plus vers le propre tampon interne du std::string, Donc le destructeur déduit que cette mémoire doit avoir a été alloué sur le tas, et essaie de delete le. Le résultat sur ma machine est un simple plantage du programme, mais le standard C++ ne se soucierait pas si des éléphants roses devaient apparaître. Le comportement est totalement indéfini.


Et c'est la raison fondamentale pour laquelle vous ne devez pas utiliser memcpy() sur des objets non copiables: vous ne savez pas si l'objet contient des pointeurs/références à ses propres membres de données, ou dépend de son propre emplacement dans mémoire d'une autre manière. Si vous memcpy() un tel objet, l'hypothèse de base que l'objet ne peut pas se déplacer en mémoire est violée, et certaines classes comme std::string S'appuient sur cette hypothèse. La norme C++ établit la distinction entre les objets (non) trivialement copiables pour éviter d'entrer dans plus de détails inutiles sur les pointeurs et les références. Il ne fait qu'une exception pour les objets trivialement copiables et dit: Eh bien, dans ce cas, vous êtes en sécurité. Mais ne me blâmez pas sur les conséquences si vous essayez de memcpy() tout autre objet.

Ce que je peux voir ici, c'est que - pour certaines applications pratiques - la norme C++ peut-être soit trop restrictive, ou plutôt pas assez permissive.

Comme indiqué dans d'autres réponses memcpy se décompose rapidement pour les types "compliqués", mais à mon humble avis, cela devrait fonctionne pour les types de mise en page standard tant que le memcpy ne rompt pas ce que font les opérations de copie et le destructeur définis du type Présentation standard. (Notez qu'une classe TC paire est autorisée pour avoir un constructeur non trivial.) La norme n'appelle explicitement que les types TC wrt. ceci, cependant.

Un projet de devis récent (N3797):

3.9 Types

...

2 Pour tout objet (autre qu'un sous-objet de classe de base) de type trivialement copiable T, que l'objet contienne ou non une valeur valide de type T, les octets sous-jacents (1.7) constituant l'objet peuvent être copiés dans un tableau de caractères ou de caractères non signés. Si le contenu du tableau de caractères ou de caractères non signés est recopié dans l'objet, l'objet conservera par la suite sa valeur d'origine. [ Exemple:

  #define N sizeof(T)
  char buf[N];        T obj; // obj initialized to its original value
  std::memcpy(buf, &obj, N); // between these two calls to std::memcpy,       
                             // obj might be modified         
  std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
                             // holds its original value 

—Fin exemple]

3 Pour tout type trivialement copiable T, si deux pointeurs vers T pointent vers des objets T distincts obj1 et obj2, où ni obj1 ni obj2 n'est une classe de base sous-objet, si les octets sous-jacents (1.7) constituant obj1 sont copiés dans obj2, obj2 conservera par la suite la même valeur que obj1. [ Exemple:

T* t1p;
T* t2p;       
     // provided that t2p points to an initialized object ...         
std::memcpy(t1p, t2p, sizeof(T));  
     // at this point, every subobject of trivially copyable type in *t1p contains        
     // the same value as the corresponding subobject in *t2p

—Fin exemple]

La norme ici parle des types trivialement copiables , mais comme a été observé par @dyp ci-dessus, il existe également types de mise en page standard qui, à ma connaissance, ne chevauchent pas nécessairement les types Trivially Copyable.

La norme dit:

1.8 Le modèle d'objet C++

(...)

5 (...) Un objet de type trivialement copiable ou de mise en page standard (3.9) doit occuper des octets de stockage contigus.

Donc, ce que je vois ici, c'est que:

  • La norme ne dit rien sur les types non copiables trivialement wrt. memcpy. (comme déjà mentionné plusieurs fois ici)
  • La norme a un concept distinct pour les types de disposition standard qui occupent un stockage contigu.
  • Le standard n'autorise pas explicitement ni interdit l'utilisation de memcpy sur des objets de disposition standard qui sont pas Facilement copiable.

Il ne semble donc pas être explicitement appelé UB, mais ce n'est certainement pas non plus ce que l'on appelle comportement non spécifié =, donc on pourrait conclure ce que @underscore_d a fait dans le commentaire de la réponse acceptée:

(...) Vous ne pouvez pas simplement dire "eh bien, il n'a pas été explicitement appelé UB, donc c'est un comportement défini!", Ce à quoi ce thread semble correspondre. N3797 3.9 points 2 ~ 3 ne définissent pas ce que memcpy fait pour les objets non copiables, donc (...) [t] chapeau est à peu près équivalent à UB à mes yeux car les deux sont inutiles pour écrire du code fiable, c'est-à-dire portable

Moi personnellement conclurait que cela équivaut à UB en ce qui concerne la portabilité (oh, ces optimiseurs), mais je pense qu'avec une certaine couverture et une connaissance de la mise en œuvre concrète, on peut s'en tirer . (Assurez-vous simplement que cela en vaut la peine.)


Note latérale: Je pense également que la norme devrait vraiment incorporer explicitement la sémantique de type de mise en page standard dans tout le désordre memcpy, car c'est une cas d'utilisation valide et utile pour faire une copie au niveau du bit d'objets non copiables, mais c'est à côté du point ici.

Lien: Puis-je utiliser memcpy pour écrire dans plusieurs sous-objets Standard Layout adjacents?

0
Martin Ba