web-dev-qa-db-fra.com

Est-il légal de réutiliser la mémoire d'un tableau de type fondamental pour un tableau de type différent (mais toujours fondamental)

Ceci fait suite à cette autre question sur la réutilisation de la mémoire. Comme la question initiale concernait une implémentation spécifique, la réponse était liée à cette implémentation spécifique.

Je me demande donc si, dans une implémentation conforme, il est légal de réutiliser la mémoire d'un tableau d'un type fondamental pour un tableau d'un type différent à condition que:

  • les deux types sont de type fondamental et ont donc un dtor trivial et un ctor par défaut
  • les deux types ont la même taille et la même exigence d'alignement

J'ai terminé avec l'exemple de code suivant:

#include <iostream>

constexpr int Size = 10;

void *allocate_buffer() {
    void * buffer = operator new(Size * sizeof(int), std::align_val_t{alignof(int)});
    int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok
    for (int i=0; i<Size; i++) in[i] = i;  // Defined behaviour because int is a fundamental type:
                                           // lifetime starts when is receives a value
    return buffer;
}
int main() {
    void *buffer = allocate_buffer();        // Ok, defined behaviour
    int *in = static_cast<int *>(buffer);    // Defined behaviour since the underlying type is int *
    for(int i=0; i<Size; i++) {
        std::cout << in[i] << " ";
    }
    std::cout << std::endl;
    static_assert(sizeof(int) == sizeof(float), "Non matching type sizes");
    static_assert(alignof(int) == alignof(float), "Non matching alignments");
    float *out = static_cast<float *>(buffer); //  (question here) Declares a dynamic float array starting at buffer
    // std::cout << out[0];      // UB! object at &out[0] is an int and not a float
    for(int i=0; i<Size; i++) {
        out[i] = static_cast<float>(in[i]) / 2;  // Defined behaviour, after execution buffer will contain floats
                                                 // because float is a fundamental type and memory is re-used.
    }
    // std::cout << in[0];       // UB! lifetime has ended because memory has been reused
    for(int i=0; i<Size; i++) {
        std::cout << out[i] << " ";         // Defined behaviour since the actual object type is float *
    }
    std::cout << std::endl;
    return 0;
}

J'ai ajouté des commentaires expliquant pourquoi je pense que ce code devrait avoir un comportement défini. Et à mon humble avis tout va bien et conforme à la norme AFAIK, mais je n’ai pas pu déterminer si la ligne marquée question ici est valide ou non.

Les objets float réutilisent la mémoire des objets int. Par conséquent, la durée de vie des ints prend fin au début de leur durée de vie, de sorte que la règle de crénelage strict ne devrait pas poser de problème. Le tableau étant alloué dynamiquement, les objets (int et floats) sont en fait tous créés dans un tableau type de vide renvoyé par operator new. Donc, je pense que tout devrait bien se passer.

Mais comme il permet le remplacement d’objets de bas niveau, ce qui est généralement mal vu en C++ moderne, je dois reconnaître que j’ai un doute ...

La question est donc la suivante: le code ci-dessus invoque-t-il UB et si oui où et pourquoi?

Déni de responsabilité: je déconseillerais ce code dans une base de code portable, et il s’agit vraiment d’une question de {juriste en matière de langage}.

15
Serge Ballesta
int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok

Correct. Mais probablement pas dans le sens que vous attendez. [expr.static.cast]

Une valeur de type «pointeur sur cv1 void» peut être convertie en une valeur de type «pointeur sur cv2 T», où T est un type d'objet et cv2 est identique à cv-qualification, ou supérieur à cv-qualification, cv1. Si la valeur du pointeur d'origine représente l'adresse A d'un octet en mémoire et que A ne satisfait pas l'exigence d'alignement de T, la valeur du pointeur résultante n'est pas spécifiée. Sinon, si la valeur du pointeur d'origine pointe sur un objet a et qu'il existe un objet b de type T (en ignorant la qualification cv) qui est un pointeur interconvertible avec a, le résultat est un pointeur sur b. Sinon, la valeur du pointeur n'est pas modifiée par la conversion.

Il n'y a pas de int ni d'objet interconvertible par un pointeur à buffer, la valeur du pointeur est donc inchangée. in est un pointeur de type int* qui pointe vers une région de mémoire brute.

for (int i=0; i<Size; i++) in[i] = i;  // Defined behaviour because int is a fundamental type:
                                       // lifetime starts when is receives a value

Est incorrect. [intro.object]

Un objet est créé par une définition, par une nouvelle expression, lors de la modification implicite du membre actif d'une union ou lors de la création d'un objet temporaire.

Absolument absent est la mission. Aucune int n'est créée. En fait, par élimination, in est un pointeur non valide , et son déréférencement est UB.

Le dernier float* suit également en tant que UB.

Même en l'absence de tous les UB susmentionnés en utilisant correctement new (pointer) Type{i}; pour créer des objets, il n'y a pas d'objet array existant. Les objets (non liés) se trouvent être côte à côte en mémoire. Cela signifie que l'arithmétique de pointeur avec le pointeur résultant est également UB. _ { [expr.add]

Lorsqu'une expression de type intégral est ajoutée ou soustraite à un pointeur, le résultat a le type de l'opérande de pointeur. Si l'expression P pointe sur l'élément x[i] d'un objet tableau x avec des éléments n, les expressions P + J et J + P (où J a la valeur j) pointent sur l'élément (éventuellement hypothétique) x[i+j] if 0 ≤ i+j ≤ n; sinon, le comportement est indéfini. De même, l'expression P - J pointe sur l'élément (éventuellement hypothétique) x[i−j] if 0 ≤ i−j ≤ n;, sinon le comportement n'est pas défini.

Où l'élément hypothétique fait référence au seul élément (hypothétique) du passé. Notez qu'un pointeur sur un élément passé-the-end qui se trouve au même emplacement d'adresse qu'un autre objet ne pointe pas vers cet autre objet.

9
Passer By

Je viens d’intervenir parce que j’ai eu l’impression qu’il restait au moins une question sans réponse, qui n’a pas été exprimée à haute voix, excuses si ce n’est pas vrai. Je pense que les gars ont brillamment répondu à la question principale de ce problème: où et pourquoi c'est un comportement indéfini; user2079303 a donné quelques idées sur la façon de le réparer. Je vais essayer de répondre à la question de savoir comment corriger le code et pourquoi il est valide. Avant de commencer à lire mon message, veuillez lire les réponses et commenter les discussions dans les réponses de Passer By et user2079303.

En gros, le problème est que les objets n’existent pas alors qu’ils n’ont besoin de rien, sauf du stockage. Ceci est dit dans la section de durée de vie de la norme, cependant, dans la section Modèle d'objet C++ avant qu'il ne soit indiqué que

Un objet est créé par une définition (6.1), par une nouvelle expression (8.3.4), lors de la modification implicite du membre actif d'une union (12.3) ou lors de la création d'un objet temporaire (7.4, 15.2).

Définition un peu délicate du concept d'objet, mais logique. Le problème est traité plus précisément dans proposition Création implicite d'objets pour la manipulation d'objets de bas niveau pour simplifier le modèle d'objet. Jusque-là, nous devrions explicitement créer un objet par les moyens mentionnés. L'un de ceux qui fonctionneront, dans ce cas, est l'expression new-placement, new-placement est une nouvelle expression n'allouant pas, qui crée un objet. Dans ce cas particulier, cela nous aidera à créer les objets de tableau manquants et les objets flottants. Le code ci-dessous montre ce que je viens d’inclure avec des commentaires et des instructions d’assemblage associées aux lignes (clang++ -g -O0 a été utilisé).

constexpr int Size = 10;

void* allocate_buffer() {

  // No alignment required for the `new` operator if your object does not require
  // alignment greater than alignof(std::max_align_t), what is the case here
  void* buffer = operator new(Size * sizeof(int));
  // 400fdf:    e8 8c fd ff ff          callq  400d70 <operator new(unsigned long)@plt>
  // 400fe4:    48 89 45 f8             mov    %rax,-0x8(%rbp)


  // (was missing) Create array of integers, default-initialized, no
  // initialization for array of integers
  new (buffer) int[Size];
  int* in = reinterpret_cast<int*>(buffer);
  // Two line result in a basic pointer value copy
  // 400fe8:    48 8b 45 f8             mov    -0x8(%rbp),%rax
  // 400fec:    48 89 45 f0             mov    %rax,-0x10(%rbp)


  for (int i = 0; i < Size; i++)
    in[i] = i;
  return buffer;
}

int main() {

  void* buffer = allocate_buffer();
  // 401047:    48 89 45 d0             mov    %rax,-0x30(%rbp)


  // static_cast equivalent in this case to reinterpret_cast
  int* in = static_cast<int*>(buffer);
  // Static cast results in a pointer value copy
  // 40104b:    48 8b 45 d0             mov    -0x30(%rbp),%rax
  // 40104f:    48 89 45 c8             mov    %rax,-0x38(%rbp)


  for (int i = 0; i < Size; i++) {
    std::cout << in[i] << " ";
  }
  std::cout << std::endl;
  static_assert(sizeof(int) == sizeof(float), "Non matching type sizes");
  static_assert(alignof(int) == alignof(float), "Non matching alignments");
  for (int i = 0; i < Size; i++) {
    int t = in[i];


    // (was missing) Create float with a direct initialization
    // Technically that is reuse of the storage of the array, hence that array does
    // not exist anymore.
    new (in + i) float{t / 2.f};
    // No new is called
    // 4010e4:  48 8b 45 c8             mov    -0x38(%rbp),%rax
    // 4010e8:  48 63 4d c0             movslq -0x40(%rbp),%rcx
    // 4010ec:  f3 0f 2a 4d bc          cvtsi2ssl -0x44(%rbp),%xmm1
    // 4010f1:  f3 0f 5e c8             divss  %xmm0,%xmm1
    // 4010f5:  f3 0f 11 0c 88          movss  %xmm1,(%rax,%rcx,4)


    // (was missing) Create int array on the same storage, default-initialized, no
    // initialization for an array of integers
    new (buffer) int[Size];
    // No code for new is generated
  }


    // (was missing) Create float array, default-initialized, no initialization for an array
    // of floats
  new (buffer) float[Size];
  float* out = reinterpret_cast<float*>(buffer);
  // Two line result in a simple pointer value copy
  // 401108:    48 8b 45 d0             mov    -0x30(%rbp),%rax
  // 40110c:    48 89 45 b0             mov    %rax,-0x50(%rbp)


  for (int i = 0; i < Size; i++) {
    std::cout << out[i] << " ";
  }
  std::cout << std::endl;
  operator delete(buffer);
  return 0;
}

Fondamentalement, toutes les expressions de nouveau placement sont omises dans le code machine, même avec -O0. Avec GCC, -O0operator new est en fait appelé et avec -O1, il est également omis. Oublions les formalités de la norme pour une seconde et pensons directement du sens pratique. Pourquoi aurions-nous besoin d'appeler les fonctions qui ne font rien, rien n'empêche que cela fonctionne sans cela, non? Parce que C++ est exactement le langage où tout le contrôle de la mémoire est donné au programme, pas à certaines bibliothèques d'exécution ou machines virtuelles, etc. Une des raisons pour lesquelles je pourrais penser ici est que le standard donne à nouveau plus de liberté aux compilateurs en matière d'optimisation. le programme à une action supplémentaire. L'idée était peut-être que le compilateur peut faire n'importe quel ordre, en omettant la magie avec le code machine, ne connaissant que la définition, la nouvelle expression, l'union, les objets temporaires comme nouveaux fournisseurs d'objets qui guident l'algorithme d'optimisation. Probablement dans la réalité, aucune optimisation de ce type ne va gâcher votre code si vous allouez de la mémoire et n'appelez pas de nouvel opérateur dessus pour des types triviaux. Fait intéressant, ces versions non allouées de new operator sont réservées et ne peuvent pas être remplacées. Peut-être est-ce que cela est censé être exactement la forme la plus simple qui informe le compilateur d’un nouvel objet.

0
Yuki