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:
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}.
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 surcv2 T
», oùT
est un type d'objet etcv2
est identique à cv-qualification, ou supérieur à cv-qualification,cv1
. Si la valeur du pointeur d'origine représente l'adresseA
d'un octet en mémoire et queA
ne satisfait pas l'exigence d'alignement deT
, la valeur du pointeur résultante n'est pas spécifiée. Sinon, si la valeur du pointeur d'origine pointe sur un objeta
et qu'il existe un objetb
de typeT
(en ignorant la qualification cv) qui est un pointeur interconvertible aveca
, le résultat est un pointeur surb
. 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émentx[i]
d'un objet tableaux
avec des élémentsn
, les expressionsP + J
etJ + 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'expressionP - 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.
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, -O0
operator 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.