web-dev-qa-db-fra.com

Un pointeur avec la bonne adresse et le bon type reste-t-il toujours un pointeur valide depuis C ++ 17?

(En référence à cette question et cette réponse .)

Avant la norme C++ 17, la phrase suivante était incluse dans [basic.compound]/ :

Si un objet de type T est situé à une adresse A, un pointeur de type cv T * dont la valeur est l'adresse A est censé pointer sur cet objet, quel que soit le mode d'obtention de la valeur.

Mais depuis C++ 17, cette phrase a été enlevée .

Par exemple, je pense que cette phrase a défini cet exemple de code et que, depuis C++ 17, il s'agit d'un comportement indéfini:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

Avant C++ 17, p1+1 Conservait l'adresse sur *p2 Et avait le bon type. Ainsi, *(p1+1) est un pointeur sur *p2. En C++ 17 p1+1 Est un pointeur au-delà de la fin , ce n'est donc pas un pointeur sur object et je crois que ce n'est pas dereferencable.

Cette interprétation de cette modification du droit standard existe-t-elle ou existe-t-il d'autres règles qui compensent la suppression de la phrase citée?

80
Oliv

Cette interprétation de cette modification du droit standard existe-t-elle ou existe-t-il d'autres règles qui compensent la suppression de la phrase citée?

Oui, cette interprétation est correcte. Un pointeur situé au-delà de la fin n'est pas simplement convertible en une autre valeur de pointeur qui pointe vers cette adresse.

Le nouveau [basic.compound]/ dit:

Chaque valeur de type pointeur est l’une des suivantes:
(3.1) un pointeur sur un objet ou une fonction (le pointeur est censé pointer sur l'objet ou la fonction), ou
(3.2) un pointeur situé au-delà de la fin d'un objet ([expr.add]), ou

Ceux-ci sont mutuellement exclusifs. p1+1 Est un pointeur situé au-delà de la fin, pas un pointeur sur un objet. p1+1 Pointe vers un hypothétique x[1] D'un tableau de taille 1 situé à p1 Et non sur p2. Ces deux objets ne sont pas convertibles en pointeur.

Nous avons également la note non normative:

[Remarque: un pointeur situé au-delà de la fin d'un objet ([expr.add]) n'est pas considéré comme pointant vers un objet du même type, non apparenté, susceptible d'être situé à cette adresse. [...]

ce qui clarifie l'intention.


Comme T.C. souligne dans de nombreux commentaires ( notamment celui-ci ), il s'agit vraiment d'un cas particulier du problème lié à la tentative d'implémentation de std::vector - c'est-à-dire que [v.data(), v.data() + v.size()) doit être une plage valide et pourtant vector ne crée pas d'objet de tableau, de sorte que la seule arithmétique de pointeur définie consisterait à passer d'un objet donné du vecteur à la dernière extrémité de sa taille hypothétique tableau. Pour plus de ressources, voir CWG 2182 , cette discussion standard , et deux révisions d'un document sur le sujet: P0593R et P0593R1 (section 1.3 spécifiquement).

44
Barry

Dans votre exemple, *(p1 + 1) = 10; devrait être UB, car il est à un bout de la fin du tableau de taille 1. Mais nous sommes dans un cas très particulier ici, parce que le tableau a été construit dynamiquement dans un tableau de caractères plus grand.

La création d'objet dynamique est décrite dans 4.5 Le modèle d'objet C++ [intro.object] , §3 du brouillon n4659 du standard C++:

3 Si un objet complet est créé (8.3.4) dans la mémoire associée à un autre objet e de type "tableau de N caractères non signés" ou de type "tableau de N std :: octets" (21.2.1), ce tableau fournit un espace de stockage pour l'objet créé si:
(3.1) - la vie de e a commencé et n’a pas pris fin, et
(3.2) - le stockage du nouvel objet s’inscrit entièrement dans e, et
(3.3) - Il n’existe pas d’objet tableau plus petit qui réponde à ces contraintes.

Le 3.3 semble plutôt incertain, mais les exemples ci-dessous clarifient l’intention:

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

Ainsi, dans l'exemple, le tableau buffer fournit un stockage pour *p1 Et *p2.

Les paragraphes suivants prouvent que l'objet complet pour *p1 Et *p2 Est buffer:

4 Un objet a est imbriqué dans un autre objet b si:
(4.1) - a est un sous-objet de b, ou
(4.2) - b fournit un espace de stockage pour a, ou
(4.3) - il existe un objet c où a est imbriqué dans c et c est imbriqué dans b.

5 Pour chaque objet x, il existe un objet appelé objet complet de x, déterminé comme suit:
(5.1) - Si x est un objet complet, alors l'objet complet de x est lui-même.
(5.2) - Sinon, l'objet complet de x est l'objet complet de l'objet (unique) qui contient x.

Une fois que ceci est établi, l’autre partie pertinente du projet n4659 pour C++ 17 est [basic.coumpound] §3 (souligner le mien):

3 ... Chaque valeur de type de pointeur est l'une des suivantes:
(3.1) - un pointeur sur un objet ou une fonction (le pointeur est censé pointer sur l'objet ou la fonction), ou
(3.2) - un pointeur situé au-delà de la fin d'un objet (8.7), ou
(3.3) - la valeur du pointeur nul (7.11) pour ce type, ou
(3.4) - une valeur de pointeur non valide.

Une valeur de type pointeur qui est un pointeur vers ou après la fin d'un objet représente l'adresse du premier octet en mémoire (4.4) occupée par l'objet ou le premier octet en mémoire après la fin de la sauvegarde). occupé par l'objet, respectivement. [Remarque: un pointeur situé au-delà de la fin d'un objet (8.7) n'est pas considéré comme pointant vers un objet non associé du type d'objet susceptible de se trouver à cette adresse. Une valeur de pointeur devient invalide lorsque le stockage qu’il indique atteint la fin de sa durée de stockage; voir 6.7. —Fin note] Pour les besoins de l'arithmétique de pointeur (8.7) et de la comparaison (8.9, 8.10), un pointeur situé après la fin du dernier élément d'un tableau x sur n éléments est considéré comme équivalent à un pointeur sur un élément hypothétique x [ n]. La représentation de la valeur des types de pointeur est définie par l'implémentation. Les pointeurs vers des types compatibles avec la présentation doivent avoir les mêmes exigences de représentation et d'alignement de valeurs (6.11) ...

La note Un pointeur au-delà de la fin ... ne s'applique pas ici car les objets pointés par p1 Et p2 et non non apparentés , mais sont imbriqués dans le même objet complet, l'arithmétique de pointeur a donc un sens à l'intérieur de l'objet fournissant le stockage: p2 - p1 est défini et est (&buffer[sizeof(int)] - buffer]) / sizeof(int) c'est-à-dire 1.

Donc p1 + 1est un pointeur sur *p2, Et *(p1 + 1) = 10; a défini un comportement et définit la valeur de *p2.


J'ai également lu l'annexe C4 sur la compatibilité entre les normes C++ 14 et les normes actuelles (C++ 17). Supprimer la possibilité d'utiliser l'arithmétique de pointeur entre des objets créés dynamiquement dans un tableau à un caractère constituerait un changement important pour lequel IMHO devrait être cité ici, car il s'agit d'une fonctionnalité couramment utilisée. Comme rien n’existe à ce sujet dans les pages de compatibilité, je pense que cela confirme que la norme n’avait pas l’intention de l’interdire.

En particulier, cela irait à l'encontre de cette construction dynamique commune d'un tableau d'objets à partir d'une classe sans constructeur par défaut:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr peut alors être utilisé comme pointeur sur le premier élément d'un tableau ...

8
Serge Ballesta

Pour développer les réponses données ici, voici un exemple de ce que je pense que le libellé révisé exclut:

Avertissement: comportement indéfini

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

Pour des raisons entièrement dépendantes de la mise en œuvre (et fragiles), la sortie possible de ce programme est:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Cette sortie montre que les deux tableaux (dans ce cas) sont stockés en mémoire, de telle sorte que 'un après la fin' de A contient la valeur de l'adresse du premier élément de B.

La spécification révisée garantit que, indépendamment du A+1 n'est jamais un pointeur valide sur B. L'ancienne phrase "quelle que soit la manière dont la valeur est obtenue" indique que si "A + 1" pointe sur "B [0]", il s'agit alors d'un pointeur valide sur "B [0]". Cela ne peut être bon et sûrement jamais l'intention.

1
Persixty