78 votes

Prendre l'adresse d'un élément d'un tableau à l'envers via un indice : légal ou non selon la norme C++ ?

J'ai vu plusieurs fois affirmer que le code suivant n'est pas autorisé par la norme C++ :

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];

Est &array[5] code C++ légal dans ce contexte ?

Je voudrais une réponse avec une référence à la norme si possible.

Il serait également intéressant de savoir s'il répond à la norme C. Et si ce n'est pas du C++ standard, pourquoi a-t-on décidé de le traiter différemment du C++ standard ? array + 5 ou &array[4] + 1 ?

7 votes

@Brian : Non, il ne faudrait vérifier les limites que si le runtime devait attraper l'erreur. Pour éviter cela, la norme peut simplement dire "non autorisé". C'est le comportement indéfini dans toute sa splendeur. Vous n'êtes pas autorisé à le faire, et le runtime et le compilateur ne sont pas tenus de le faire. dire à vous si vous le faites.

0 votes

Ok, juste pour clarifier un peu, parce que le titre m'a induit en erreur : Un pointeur qui dépasse d'une unité la fin d'un tableau n'est pas hors limites. Les pointeurs hors limites ne sont pas autorisés en général, mais la norme est beaucoup plus indulgente avec les pointeurs un au-delà de la fin. Vous pourriez vouloir modifier le titre si vous posez une question spécifique sur les pointeurs un-passé-de-la-fin. Si vous voulez en savoir plus sur les pointeurs hors limites en général vous devriez modifier votre exemple ;)

0 votes

Il ne demande pas de conseils sur le passé en général. Il demande comment utiliser l'opérateur & pour obtenir le pointeur.

44voto

Adam Rosenfield Points 176408

Oui, c'est légal. Depuis le Projet de norme C99 :

§6.5.2.1, paragraphe 2 :

Une expression postfixe suivie d'une expression entre crochets [] est un indice d'un élément d'un tableau. La définition de l'opérateur d'indice [] c'est que E1[E2] est identique à (*((E1)+(E2))) . En raison des règles de conversion qui qui s'appliquent au format binaire + opérateur, si E1 est un objet de type tableau (de manière équivalente, un pointeur vers le fichier élément initial d'un objet tableau) et E2 est un nombre entier, E1[E2] désigne le E2 -th élément de E1 (en comptant à partir de zéro).

§6.5.3.2, paragraphe 3 (c'est moi qui souligne) :

L'unaire & L'opérateur donne l'adresse de son opérande. Si l'opérande a le type '' type '', le résultat est de type ''pointeur vers type ''. Si l'opérande est le résultat d'une opération unaire * opérateur, ni cet opérateur ni le & est évalué et le résultat est le même que si les deux étaient omis, sauf que les contraintes sur les opérateurs s'appliquent toujours et que le résultat n'est pas une lvalue. De même, *si l'opérande est le résultat d'un [] ni l'opérateur & ni l'opérateur unaire `qui est impliquée par le[]est évaluée et le résultat est comme si le&opérateur ont été supprimés et l[]ont été transformés en un+` opérateur** . Sinon, le résultat est un pointeur sur l'objet ou la fonction désigné par son opérande.

§6.5.6, paragraphe 8 :

Lorsqu'une expression de type entier est ajoutée ou soustraite d'un pointeur, le résultat a le type de l'opérande du pointeur. résultat a le type de l'opérande du pointeur. Si l'opérande du pointeur pointe sur un élément de d'un tableau, et que le tableau est suffisamment grand, le résultat pointe vers un élément décalé de l'élément d'origine de telle sorte que la différence entre les indices des éléments du tableau résultant et d'origine soit égale à l'expression entière. d'origine soit égale à l'expression entière. En d'autres termes, si l'expression P points à le site i -d'un objet tableau, les expressions (P)+N (de manière équivalente, N+(P) ) et (P)-N (où N a la valeur n ) pointent, respectivement, vers le i+n - et i−n -Les éléments de de l'objet tableau, à condition qu'ils existent. De plus, si l'expression P pointe vers le dernier élément d'un tableau, l'expression (P)+1 pointe une fois le dernier élément de l'objet et si l'expression Q pointe une fois le dernier élément d'un tableau, l'expression (Q)-1 pointe vers le dernier élément de l'objet tableau. Si le pointeur et le résultat pointent tous deux sur des éléments du même objet tableau, ou sur un élément postérieur au dernier élément de l'objet tableau, l'évaluation ne doit pas produire de surcharge. dernier élément de l'objet tableau, l'évaluation ne produira pas de débordement. comportement est indéfini. Si le résultat pointe sur l'un des derniers éléments de l'objet tableau, il ne doit pas être utilisé comme opérande d'un tableau unaire. * qui est évalué.

Notez que la norme autorise explicitement les pointeurs à pointer un élément au-delà de la fin du tableau, à condition qu'ils ne soient pas déréférencés . Par les points 6.5.2.1 et 6.5.3.2, l'expression &array[5] est équivalent à &*(array + 5) ce qui est équivalent à (array+5) qui pointe une fois la fin du tableau. Ceci ne résulte pas en une déréférence (par 6.5.3.2), donc c'est légal.

0 votes

Intéressant, donc c'est légal et explicitement bien défini en C qui mai être différente de celle du C++ (voir d'autres discussions !).

1 votes

Il a explicitement demandé le C++. C'est le genre de différence subtile sur laquelle on ne peut pas compter lors du portage entre les deux.

3 votes

Il a posé des questions sur les deux : "Il serait également intéressant de savoir si elle répond à la norme C."

41voto

jalf Points 142628

Votre exemple est légal, mais seulement parce que vous n'êtes pas vraiment à l'aide d'un hors limites pointeur. Occupons-nous en dehors des limites des pointeurs d'abord: parce que c'est la façon dont je l'ai d'abord interprété votre question, avant j'ai remarqué que l'exemple utilise un passé la fin de pointeur à la place ;))

En général, vous n'êtes même pas autorisés à créer un hors-limites pointeur. Un pointeur doit pointer vers un élément dans le tableau, ou un passé la fin. Nulle part ailleurs.

Le pointeur n'est même pas autorisé à exister, ce qui signifie que vous êtes évidemment pas autorisé à déréférencer il soit. ;)

Voici ce que le standard a à dire sur le sujet:

5.7:5:

Quand une expression intégrale le type est ajouté ou soustrait un pointeur, le résultat est du type de le pointeur de l'opérande. Si le pointeur opérande points à un élément d'un objet de tableau, et le tableau est grand assez, le résultat des points à un élément de décalage à partir de l'original élément, telles que la différence de les indices de la suite et d'origine les éléments du tableau est égale à la expression intégrale. En d'autres termes, si l'expression de P points à la i-ème élément d'un tableau d'objet, l' les expressions (P)+N (de manière équivalente, N+(P)) et (P)-N (où N est le la valeur de n), respectivement, le i+n-th et th éléments de la objet de tableau, à condition qu'ils existent. En outre, si l'expression de P points le dernier élément d'un tableau l'objet, l'expression (P)+1 points un passé le dernier élément du tableau objet, et si l'expression de Q points un passé le dernier élément d'un tableau l'objet, l'expression (Q)-1 points à le dernier élément de l'objet array. Si le pointeur de l'opérande et le résultat de point d'éléments de la même objet de tableau, ou un passé la dernière élément de l'objet tableau, le l'évaluation ne doit pas produire un trop-plein; autrement, le comportement est undefined.

(l'emphase est mienne)

Bien sûr, c'est pour l'opérateur+. Donc, juste pour être sûr, voici ce que dit la norme sur les indices de tableaux:

5.2.1:1:

L'expression E1[E2] est identique (par définition) *((E1)+(E2))

Bien sûr, il y a une mise en garde évidente: Votre exemple n'est pas réellement montrer une des limites d'un pointeur. il utilise un "un passé la fin de" pointeur, ce qui est différent. Le pointeur est autorisé à exister (comme ci-dessus, dit), mais la norme, aussi loin que je peux voir, ne dit rien à propos de déréférencement. Le plus proche que je peux trouver est 3.9.2:3:

[Note: par exemple, l'adresse d'un passé la fin d'un tableau (5.7) serait considéré comme point à un autre objet du tableau du type d'élément qui peut être situé à cette adresse. -la note de fin ]

Ce qui me semble impliquer que, oui, vous pouvez légalement déréférencement d'elle, mais le résultat de la lecture ou de l'écriture à l'emplacement n'est pas spécifié.

Grâce à ilproxyil pour la correction de la dernière peu ici, de répondre à la dernière partie de votre question:

  • array + 5 n'est pas réellement déréférencement de rien, c'est tout simplement crée un pointeur vers un passé la fin d' array.
  • &array[4] + 1déréférence array+4 (ce qui est parfaitement sûr), prend l'adresse de la lvalue, et ajoute un à cette adresse, qui résultats dans un passé la fin de pointeur (mais que le pointeur n'est jamais déréférencé.
  • &array[5] déréférence tableau+5 (autant que je peux voir, c'est légal, et les résultats dans "un autre objet de la matrice du type d'élément", comme l' ci-dessus dit), et prend alors le l'adresse de cet élément, qui a également semble juridique suffisant.

Donc ils n'ont pas tout à fait la même chose, mais dans ce cas, le résultat final est le même.

4 votes

&array[5] pointe vers un passé. Cependant, ce n'est pas une façon légale d'obtenir cette adresse.

1 votes

D'accord, je dirais que &array[5] est UB. (même si cela peut fonctionner comme prévu en pratique).

0 votes

@Evan : Oui, je m'en suis rendu compte aussi, et j'ai modifié mon message. Notez que le titre de la question pose la question de la sortie de limites. La réponse devrait décrire les deux cas maintenant.

17voto

Tyler McHenry Points 35551

Il est légal.

Selon la documentation de gcc pour C++ , &array[5] est légal. Dans les deux cas, C++ et en C vous pouvez sans risque vous adresser à l'élément situé une fois après la fin d'un tableau - vous obtiendrez un pointeur valide. Ainsi, &array[5] en tant qu'expression est légal.

Cependant, la tentative de déréférencer des pointeurs vers de la mémoire non allouée reste un comportement non défini, même si le pointeur pointe vers une adresse valide. Ainsi, tenter de déréférencer le pointeur généré par cette expression est toujours un comportement non défini (c'est-à-dire illégal) même si le pointeur lui-même est valide.

En pratique, j'imagine que cela ne provoquera pas de crash.

Edit : A propos, c'est généralement de cette manière que l'itérateur end() des conteneurs STL est implémenté (comme un pointeur vers un-past-the-end), donc c'est un bon témoignage de la légalité de la pratique.

Edit : Oh, maintenant je vois que vous ne demandez pas vraiment si détenir un pointeur à cette adresse est légal, mais si cette façon exacte d'obtenir le pointeur est légale. Je m'en remets aux autres personnes qui ont répondu à cette question.

4 votes

Je dirais que vous avez raison, si et seulement si la spécification C++ ne dit pas que &* doit être traité comme un no-op. J'imagine qu'elle ne le dit probablement pas.

8 votes

A page à laquelle vous faites référence (correctement) indique qu'il est légal de point un après la fin. &array[5], techniquement, déréférence d'abord (array + 5), puis le référence à nouveau. Donc techniquement, c'est comme ceci : (&*(tableau + 5)). Heureusement, les compilateurs sont assez intelligents pour savoir que &* peut être factorisé en rien. Cependant, ils ne font pas ont pour faire ça, donc, je dirais que c'est UB.

4 votes

Evan : Il y a plus que ça. Regarde la dernière ligne du numéro de base 232 : std.dkuug.dk/JTC1/SC22/WG21/docs/cwg_active.html#232 . Le dernier exemple semble erroné, mais il explique clairement que la distinction se fait au niveau de la conversion "lvalue-to-rvalue", qui n'a pas lieu dans ce cas.

13voto

Juste pour mettre tout cela ensemble et pour que nous puissions comparer les différentes idées qui sont apparues dans les différentes réponses. Je vais commenter ce que je pense de tout ça. wiki communautaire, car il s'agit simplement d'une collection de pensées d'autres personnes :) Tous les accents sont mis par moi ci-dessous.

Tout d'abord, nous devons nous préoccuper de savoir si le pointeur sur l'avant-dernier élément fait référence à un objet. Un tableau d'objets liés N a N des sous-objets qui sont ses éléments, comme expliqué dans la section 8.3.4/1

Un objet de type tableau contient un ensemble non vide, alloué de manière contiguë, de N sous-objets de type T. - 8.3.4/1

A ma connaissance, il n'y a aucune mention dans la norme d'un objet situé juste après un tableau. Si s'il existe un tel objet, nous sommes autorisés à déréférencer le pointeur qui pointe une fois après la fin, en raison du texte et de la note de clarification suivants

Si un objet de type T est situé à une adresse A On dit d'un pointeur de type cv T* dont la valeur est l'adresse A qu'il pointe vers cet objet, quelle que soit la façon dont cette valeur a été obtenue. [Note : par exemple, l'adresse un après la fin d'un tableau (5.7) serait considérée comme pointant vers un objet non lié au type d'élément du tableau, à savoir pourrait être situé à cette adresse. ] - 3.9.2/3

Cela veut dire que que ce qui suit est bien défini, si l'implémentation pose les objets de manière à ce que le stockage des b est alloué directement derrière l'objet tableau (que vous pouvez obtenir manuellement si vous surallouez une partie de la mémoire en utilisant malloc, en assignant un pointeur à un tableau ayant une taille plus petite - je vais rester simple et illustrer seulement en utilisant l'exemple suivant)

int a[3], b;
*(a + 3) = 0;
assert(b == 0 && (a + 3 == &b) && a[3] == 0);

Le consentement sur quelques personnes est que votre expression montrée, &array[5] c'est comportement indéfini . Ceci est basé sur le fait, qui demeure, que la Norme dit à 3.10/2 y 5.3.1/1

Une lvalue fait référence à un objet ou à une fonction. - 3.10/2

L'opérateur unaire * effectue une indirection : l'expression à laquelle il est appliqué est un pointeur vers un type d'objet ou un pointeur vers un type de fonction et le résultat est une lvalue se référant à l'objet ou à la fonction vers laquelle l'expression pointe. - 5.3.1/1

Ci-dessus, nous avons vu que nous n'avons pas la garantie qu'il existe un objet (du même type) après le dernier élément d'un tableau alloué. Ceci doit être distingué d'un autre cas, qui se produit lorsque vous avez un objet alloué (mémoire réservée), mais que cet objet n'a pas encore commencé à vivre, comme cela se produit si vous allouez de la mémoire avec malloc, et que vous allez placer-nouveau un objet dans cette zone : Dans ce cas, vous êtes autorisé à déréférencer la zone. avant vous invoquez le constructeur, pour autant que vous respectiez certaines règles simples, comme ne pas essayer de lire une valeur à partir de la valeur lval générée ( 3.8/5 y 3.8/6 )

Ce qui est intéressant, c'est de savoir ce qui se passe quand la lvalue fait no se réfère à un objet ? Rappelez-vous qu'une lvalue a pour faire référence à un objet (ou une fonction).

La norme dessine cette opération bien définie à 5.2.8/2 en parlant de la typeid qui évalue les opérandes d'expression de valeur l.

Si l'expression lvalue est obtenue en appliquant l'opérateur unaire * à un pointeur et que le pointeur est une valeur nulle (4.10), l'expression typeid lève l'exception bad_typeid. - 5.3.1/1

Ceci est contraire à 3.10/2 qui exige qu'une expression lvalue se réfère à un objet ou à une fonction, ce qui n'est pas le cas d'un pointeur nul. À ce stade, nous avons un défaut dans la norme : Un endroit permet de déréférencer un pointeur nul d'une manière qui contredit une autre partie de la norme. Cela a été observé il y a longtemps et fait l'objet d'une discussion dans le rapport sur les questions liées. Comme le fait remarquer l'un d'entre eux, il s'agit simplement de gérer les nullités déréférencées spéciales, afin de contourner le problème des lvalues sans objet. Puisque cela commence par parler d'une lvalue, c'est au moins une façon problématique de la gérer actuellement.

L'idée pour gérer cela de manière générale, est d'introduire une lvalue vide qui ne se réfère volontairement à aucun objet ou fonction. Si nous essayons d'y lire une valeur, nous obtenons un comportement indéfini. Tant que nous ne le faisons pas, nous ne le faisons pas. Le déréférencement d'une adresse passée à la fin pourrait produire une telle lvalue vide, puisque nous ne pouvons pas être sûrs qu'il y ait un objet localisé ou non.

Cependant, comme l'indiquent les discussions sur ce rapport, il reste encore des questions en suspens (par exemple, que se passe-t-il avec notre cas de surallocation ?) avant qu'il puisse être intégré dans la norme.

Conclusion

Je crois qu'il n'y a ni bonne ni mauvaise façon de faire. Bien que j'aie une légère tendance à considérer cela comme un comportement généralement non défini, parce qu'il n'y a pas de valeur l que n'a pas se référer à un objet, je vois aussi la manière actuelle, assez problématique, de typeid la gestion de ce problème. Comme il s'agit d'un problème actif dans la norme, le mieux que vous puissiez faire est d'effectuer une addition pour obtenir la valeur du pointeur, au lieu de déréférencer après la fin, évitant ainsi complètement le problème.

Notez que tout ce qui précède n'est pas un problème en C. Le C rend tout cela bien formé en disant &* est proche d'un no-op mais fait juste d'un pointeur une valeur r, donc vous ne pouvez pas faire

(&*a) = NULL;

Malheureusement, la même chose n'est pas vraie pour le C++.

9voto

Charles Bailey Points 244082

Je ne pense pas que ce soit illégal, mais je pense que le comportement de &array[5] est indéfini.

  • 5.2.1 [expr.sub] E1[E2] est identique (par définition) à *((E1)+(E2))

  • 5.3.1 [expr.unary.op] opérateur unaire * ... le résultat est une lvalue faisant référence à l'objet ou à la fonction vers laquelle l'expression pointe.

À ce stade, vous avez un comportement indéfini parce que l'expression ((E1)+(E2)) n'a pas réellement pointé vers un objet et la norme indique quel devrait être le résultat à moins que ce ne soit le cas.

  • 1.3.12 [defns.undefined] Un comportement non défini peut également être attendu lorsque la présente Norme internationale omet la description de toute définition explicite du comportement.

Comme indiqué ailleurs, array + 5 y &array[0] + 5 sont des moyens valides et bien définis d'obtenir un pointeur un au-delà de la fin du tableau.

0 votes

Le point clé est : "le résultat de '*' est une lvalue". D'après ce que je peux dire, cela ne devient UB que si vous avez une conversion de lvalue en rvalue sur ce résultat.

1 votes

Je soutiens que le résultat de '*' n'est défini qu'en fonction de l'objet auquel l'expression à laquelle l'opérateur est appliqué, alors il est indéfini - par omission - quel est le résultat si l'expression n'a pas une valeur qui se réfère effectivement à un objet. C'est loin d'être clair, cependant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X