web-dev-qa-db-fra.com

Std :: launder peut-il être utilisé pour convertir un pointeur d'objet en son pointeur de tableau englobant?

Le projet de norme actuel (et vraisemblablement C++ 17) dit dans [composé base/4] :

[Remarque: un objet de tableau et son premier élément ne sont pas interconvertibles par un pointeur, même s'ils ont la même adresse. - note de fin]

Donc, un pointeur sur un objet ne peut pas être reinterpret_cast 'd pour obtenir son pointeur de tableau englobant.

Maintenant, il y a std::launder, [ptr.launder/1] :

template<class T> [[nodiscard]] constexpr T* launder(T* p) noexcept;

Requiert: p représente l'adresse A d'un octet en mémoire. Un objet X qui est dans sa durée de vie et dont le type est similaire à T se trouve à l'adresse A. Tous les octets de stockage qui seraient accessibles via le résultat sont accessibles via p (voir ci-dessous).

Et la définition de accessible est dans [ptr.launder/3] :

Remarques: Un appel de cette fonction peut être utilisé dans une expression constante principale chaque fois que la valeur de son argument peut être utilisée dans une expression constante principale. Un octet de stockage est accessible via une valeur de pointeur qui pointe vers un objet Y s'il se trouve dans la mémoire occupée par Y, un objet pointeur-interconvertible avec Y ou l'objet qui entoure immédiatement si Y est un tableau. élément. Le programme est mal formé si T est un type de fonction ou cv void.

À première vue, il semble que std::launder puisse être utilisé pour effectuer la conversion susmentionnée, en raison de la partie sur laquelle j'ai insisté.

Mais. Si p pointe sur un objet d'un tableau, les octets du tableau sont accessible selon cette définition (même si p n'est pas convertible en tableau), tout comme le résultat du launder. Il semble donc que la définition ne dit rien sur ce problème.

Donc, std::launder peut-il être utilisé pour convertir un pointeur d'objet en son pointeur de tableau englobant?

12
geza

Cela dépend si l'objet de tableau englobant est un objet complet et, dans le cas contraire, si vous pouvez accéder valablement à davantage d'octets via un pointeur vers cet objet de tableau englobant (par exemple, parce que c'est un élément de tableau lui-même ou un pointeur interconvertible avec un objet plus grand , ou un pointeur-interconvertible avec un objet qui est un élément de tableau). L'exigence "accessible" signifie que vous ne pouvez pas utiliser launder pour obtenir un pointeur qui vous permettrait d'accéder à plus d'octets que ne le permet la valeur du pointeur source, sous peine d'un comportement non défini. Cela garantit que la possibilité qu'un code inconnu puisse appeler launder n'affecte pas l'analyse d'échappement du compilateur.

Je suppose que quelques exemples pourraient aider. Chaque exemple ci-dessous reinterpret_casts a int* pointant vers le premier élément d'un tableau de 10 ints dans une int(*)[10]. Comme ils ne sont pas interconvertibles par un pointeur, le reinterpret_cast ne modifie pas la valeur du pointeur et vous obtenez un int(*)[10] avec la valeur "pointeur vers le premier élément de (quel que soit le tableau)". Chaque exemple tente ensuite d’obtenir un pointeur sur l’ensemble du tableau en appelant std::launder sur le pointeur de conversion.

int x[10];
auto p = std::launder(reinterpret_cast<int(*)[10]>(&x[0])); 

C'est acceptable; vous pouvez accéder à tous les éléments de x par le biais du pointeur source et le résultat de launder ne vous permet pas d'accéder à autre chose.

int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0])); 

Ceci est indéfini. Vous ne pouvez accéder aux éléments de x2[0] que par le pointeur source, mais le résultat (qui serait un pointeur sur x2[0]) vous aurait permis d'accéder à x2 [1], ce que vous ne pouvez pas utiliser avec la source.

struct X { int a[10]; } x3, x4[2]; // assume no padding
auto p3 = std::launder(reinterpret_cast<int(*)[10]>(&x3.a[0])); // OK

C'est acceptable. Encore une fois, vous ne pouvez pas accéder à x3.a à un octet auquel vous n’avez pas déjà accès via un pointeur.

auto p4 = std::launder(reinterpret_cast<int(*)[10]>(&x4[0].a[0])); 

Ceci est (destiné à être) indéfini. Vous auriez pu atteindre x4[1] à partir du résultat, car x4[0].a est un pointeur interconvertible avec x4[0]. Un pointeur sur le premier peut donc être reinterpret_cast pour renvoyer un pointeur sur le dernier, qui peut ensuite être utilisé pour l'arithmétique du pointeur. Voir https://wg21.link/LWG2859

struct Y { int a[10]; double y; } x5;
auto p3 = std::launder(reinterpret_cast<int(*)[10]>(&x5.a[0])); 

Et ceci est encore une fois non défini, car vous auriez pu atteindre x5.y à partir du pointeur résultant (de reinterpret_cast à un Y*) mais le pointeur source ne peut pas être utilisé pour y accéder.

7
T.C.

Remarque: tout compilateur non schizophrène acceptera probablement cela volontiers, comme il accepterait une distribution de style C ou une interprétation réinterprétée.

Mais à mon humble avis, la réponse à votre question est non. L'objet de tableau souligné englobant immédiatement si Y est un élément de tableau réside dans un Remarque paragraphe, pas dans le Requiert un. Cela signifie que tant que la section requise est respectée, les remarques que l'on applique également. Comme un tableau et son type d'élément ne sont pas des types similaires, l'exigence n'est pas satisfaite et std::launder ne peut pas être utilisé.

Ce qui suit est plutôt une interprétation générale (philosophique?). À l’époque de K & R C (dans les années 70), C était censé remplacer le langage de l’Assemblée. Pour cette raison, la règle était la suivante: le compilateur doit obéir au programmeur à condition que le code source puisse être traduit. Donc, aucune règle de crénelage stricte et un pointeur n'était plus une adresse avec des règles arithmétiques supplémentaires. Cela a fortement changé en C99 et C++ 03 (sans parler de C++ 11 +). Les programmeurs sont maintenant supposés utiliser le C++ en tant que langage de haut niveau. Cela signifie qu'un pointeur est juste un objet qui permet d'accéder à un autre objet d'un type donné, et un tableau et son type d'élément sont des types totalement différents. Les adresses de mémoire sont maintenant un peu plus que des détails d'implémentation. Tenter de convertir un pointeur en un tableau en un pointeur en premier élément va donc à l’encontre de la philosophie du langage et risque de faire mal au programmeur dans une version ultérieure du compilateur. Bien sûr, les compilateurs réels l'acceptent toujours pour des raisons de compatibilité, mais nous ne devrions même pas essayer de l'utiliser dans des programmes modernes.

2
Serge Ballesta