Przypisanie mamy:

char* a;
int   i;

Wiele wprowadzeń do C ++ (np. Ten ) sugeruje, że rvalues a+i i &a[i] są wymienne. Naiwnie wierzyłem na to przez kilka dziesięcioleci, dopóki niedawno natknąłem się na następujący tekst ([DCl.REF]:

W szczególności, nie może istnieć w dobrze zdefiniowanym programie, ponieważ jedynym sposobem tworzenia takiego odniesienia byłoby wiązanie go do "obiektu" uzyskanego przez dewelowanie wskaźnika zerowego, co powoduje niezdefiniowane zachowanie.

Innymi słowy, "wiążący" obiekt odniesienia do null-dereference powoduje niezdefiniowane zachowanie. Na podstawie kontekst powyższego tekstu, jeden naprzeciw, który jedynie oceniający &a[i] ( W offsetof makro) jest uważany za "wiążące" odniesienie. Ponadto wydaje się być konsensus, że &a[i] powoduje niezdefiniowane zachowanie w przypadku, gdy a=null i i=0. To zachowanie różni się od a+i (przynajmniej w C ++, w przypadku A = NULL, I = 0 Przypadek).

Prowadzi to do co najmniej 2 pytań dotyczących różnic między a+i i &a[i]:

Po pierwsze, co jest podstawą podkładową różnica semantyczna między a+i i &a[i] powoduje to różnicę w zachowaniu. Czy można go wyjaśnić pod względem wszelkiego rodzaju ogólnych zasad, nie tylko "wiążące odniesienie do obiektu Dereference NULL powoduje niezdefiniowane zachowanie tylko dlatego, że jest to bardzo konkretny przypadek, że wszyscy wiedzą"? Czy to tak, że &a[i] może wygenerować dostęp do pamięci do a[i]? Czy autor specyfikacji nie był zadowolony z Null Dereferences tego dnia? Albo coś innego?

Po drugie, oprócz przypadku, w którym a=null i i=0, czy są jakieś inne przypadki, w których a+i i &a[i] zachowują się inaczej? (może być objęty pierwszym pytaniem, w zależności od odpowiedzi na nie.)

9
personal_cloud 1 marzec 2019, 08:45

1 odpowiedź

Tl; dr: {x0}} i &a[i] są zarówno dobrze uformowane i wytwarzają wskaźnik zerowy, gdy a jest wskaźnikiem NULL i i wynosi 0, zgodnie z (intencją ) Standard i wszyscy kompilatowie zgadzają się.


a+i jest oczywiście dobrze uformowany na [Expr.add] / 4 najnowszego standardu:

Gdy wyrażenie j, które ma typ integralny, dodaje się do lub odejmuje się z wyrażenia p p-wskaźnika, wynik ma typ P.

  • Jeśli P ocenia wartość wskaźnika zerowej i J oceniono na 0, wynikiem jest wartość wskaźnika zerowej.
  • […]

&a[i] jest trudna. Na [Expr.sub] / 1, {{ X1}} jest równoważny *(a+i), a więc &a[i] jest równoważny &*(a+i). Teraz standard nie jest całkiem jasny, czy &*(a+i) jest dobrze uformowany, gdy a+i jest wskaźnikiem zerowy. Ale jak @ n.m. wskazuje w Komentarz, zamiar nagrany w CWG 232 ma pozwolić na ten przypadek.


Ponieważ podstawowy język UB musi zostać złapany w ciągłym wyrażeniu ([Expr.Const] /(4.6)), możemy przetestować, czy kompilatory myślą, że te dwa wyrażenia są UB.

Oto demo, jeśli kompilatory myślą, że stałą ekspresję w static_assert jest UB, lub jeśli uważają, że wynik nie jest true, muszą wytworzyć diagnostykę (błąd lub ostrzeżenie) na standard:

(Należy pamiętać, że wykorzystuje to pojedynczy parametr statyczny_Sert i Constexpr Lambda, które są funkcjami C ++ 17, a domyślny argument Lambda, który jest również dość nowy)

static_assert(nullptr == [](char* a=nullptr, int i=0) {
    return a+i;
}());

static_assert(nullptr == [](char* a=nullptr, int i=0) {
    return &a[i];
}());

Z https://godbolt.org/Z/HHSV4I, wydaje się, że wszyscy kompilatory zachowują się równomiernie w tym przypadku , Wytwarzając bez diagnostyki w ogóle (co mnie nieco zaskakuje).


Jest jednak inny niż offset. Wdrożenie Wysłane w To pytanie wyraźnie tworzy Odniesienie (które jest niezbędne do sidestep zdefiniowanego przez użytkownika operator&), a zatem podlega wymaganiom od referencji.

7
cpplearner 1 marzec 2019, 09:22