Niezdefiniowane zachowanie i punkty sekwencji zostały ponownie załadowane


84

Rozważ ten temat jako kontynuację następującego tematu:

Poprzednia rata
Niezdefiniowane zachowanie i punkty sekwencji

Wróćmy do tego zabawnego i zawiłego wyrażenia (wyrażenia zapisane kursywą pochodzą z powyższego tematu * uśmiech *):

i += ++i;

Mówimy, że wywołuje to niezdefiniowane zachowanie. Przypuszczam, że kiedy powiem, możemy założyć, że niejawnie typu o ito jeden z wbudowanych typów.

Co zrobić, jeśli typ o ito typ zdefiniowany przez użytkownika? Powiedz, że jego typ jest Indexzdefiniowany w dalszej części tego postu (patrz poniżej). Czy nadal wywoływałoby niezdefiniowane zachowanie?

Jeśli tak, dlaczego? Czy nie jest to równoznaczne z pisaniem i.operator+=(i.operator++());lub nawet prostsze składniowo i.add(i.inc());? A może one również wywołują niezdefiniowane zachowanie?

Jeśli nie, dlaczego nie? W końcu obiekt ijest modyfikowany dwukrotnie między kolejnymi punktami sekwencji. Przypomnij sobie praktyczną zasadę: wyrażenie może modyfikować wartość obiektu tylko raz między kolejnymi "punktami sekwencji . A jeśli i += ++ijest wyrażeniem, to musi wywołać niezdefiniowane zachowanie. Jeśli tak, to jego odpowiedniki, i.operator+=(i.operator++());a i.add(i.inc());także musi wywołać niezdefiniowane zachowanie, które wydaje się nieprawdziwe! (o ile rozumiem)

A i += ++imoże nie jest to wyrażenie na początek? Jeśli tak, to co to jest i jaka jest definicja wyrażenia ?

Jeśli jest to wyrażenie, a jednocześnie jego zachowanie jest również dobrze zdefiniowane, oznacza to, że liczba punktów sekwencji powiązanych z wyrażeniem zależy w jakiś sposób od typu operandów zaangażowanych w wyrażenie. Czy mam rację (choćby częściowo)?


A propos, co powiesz na to wyrażenie?

//Consider two cases:
//1. If a is an array of a built-in type
//2. If a is user-defined type which overloads the subscript operator!

a[++i] = i; //Taken from the previous topic. But here type of `i` is Index.

Musisz to również wziąć pod uwagę w swojej odpowiedzi (jeśli na pewno znasz jej zachowanie). :-)


Jest

++++++i;

dobrze zdefiniowane w C ++ 03? W końcu to jest to,

((i.operator++()).operator++()).operator++();

class Index
{
    int state;

    public:
        Index(int s) : state(s) {}
        Index& operator++()
        {
            state++;
            return *this;
        }
        Index& operator+=(const Index & index)
        {
            state+= index.state;
            return *this;
        }
        operator int()
        {
            return state;
        }
        Index & add(const Index & index)
        {
            state += index.state;
            return *this;
        }
        Index & inc()
        {
            state++;
            return *this;
        }
};

13
+1 świetne pytanie, które zainspirowało świetne odpowiedzi. Czuję, że powinienem powiedzieć, że to wciąż straszny kod, który należy refactored być bardziej czytelny, ale pewnie wiecie, że w każdym razie :)
Philip Potter

4
@ Co to za pytanie: kto powiedział, że to to samo? lub kto powiedział, że to nie to samo? Czy to nie zależy od tego, jak je wdrożysz? (Uwaga: zakładam, że typ sjest typem zdefiniowanym przez użytkownika!)
Nawaz

5
Nie widzę żadnego obiektu skalarnego modyfikowanego dwukrotnie między dwoma punktami sekwencji ...
Johannes Schaub - litb

3
@Johannes: w takim razie chodzi o obiekt skalarny . Co to jest? Zastanawiam się, dlaczego nigdy wcześniej o tym nie słyszałem. Może dlatego, że samouczki / C ++ - faq o tym nie wspominają, czy nie podkreślają tego? Czy różni się od obiektów typu wbudowanego ?
Nawaz

3
@Phillip: Oczywiście nie mam zamiaru pisać takiego kodu w prawdziwym życiu; w rzeczywistości żaden rozsądny programista nie napisze tego. Te pytania są zwykle opracowywane, abyśmy mogli lepiej zrozumieć całą sprawę z niezdefiniowanych zachowań i punktów sekwencji! :-)
Nawaz

Odpowiedzi:


48

Wygląda na kod

i.operator+=(i.operator ++());

Działa doskonale w odniesieniu do punktów sekwencji. Sekcja 1.9.17 normy C ++ ISO mówi tak o punktach sekwencji i ocenie funkcji:

Podczas wywoływania funkcji (niezależnie od tego, czy funkcja jest wbudowana, czy nie), po ocenie wszystkich argumentów funkcji (jeśli istnieją) występuje punkt sekwencji, który ma miejsce przed wykonaniem jakichkolwiek wyrażeń lub instrukcji w treści funkcji. Istnieje również punkt sekwencji po skopiowaniu zwracanej wartości i przed wykonaniem jakichkolwiek wyrażeń poza funkcją.

Oznaczałoby to na przykład, że i.operator ++()parametr as operator +=ma punkt sekwencji po dokonaniu oceny. Krótko mówiąc, ponieważ przeciążone operatory są funkcjami, obowiązują normalne zasady sekwencjonowania.

Swoją drogą, świetne pytanie! Naprawdę podoba mi się sposób, w jaki zmuszasz mnie do zrozumienia wszystkich niuansów języka, o którym już myślałem, że znam (i myślałem, że myślałem, że znam). :-)



11

Jak powiedzieli inni, twój i += ++iprzykład działa z typem zdefiniowanym przez użytkownika, ponieważ wywołujesz funkcje, a funkcje zawierają punkty sekwencji.

Z drugiej strony a[++i] = inie ma tyle szczęścia, zakładając, że ajest to podstawowy typ tablicy, a nawet typ zdefiniowany przez użytkownika. Problem polega na tym, że nie wiemy, która część wyrażenia zawierającego ijest oceniana jako pierwsza. Może się zdarzyć, że ++izostanie oszacowana, przekazana do operator[](lub wersja surowa) w celu pobrania tam obiektu, a następnie wartość izostanie przekazana do tego (czyli po zwiększeniu i). Z drugiej strony, być może druga strona jest najpierw oceniana, przechowywana do późniejszego przypisania, a następnie ++iczęść jest oceniana.


więc ... czy wynik jest zatem nieokreślony, a nie UB, skoro kolejność oceny wyrażeń jest nieokreślona?
Philip Potter

@Philip: nieokreślony oznacza, że ​​oczekujemy, że kompilator określi zachowanie, podczas gdy undefined nie nakłada takiego obowiązku. Myślę, że jest tutaj niezdefiniowane, aby dać kompilatorom więcej miejsca na optymalizacje.
Matthieu M.

@Noah: Wysłałem również odpowiedź. Sprawdź to i daj mi znać, co myślisz. :-)
Nawaz

1
@Philip: wynik to UB, ze względu na regułę w 5/4: „Wymagania tego paragrafu powinny być spełnione dla każdego dopuszczalnego uporządkowania podwyrażeń pełnego wyrażenia; w przeciwnym razie zachowanie jest niezdefiniowane.”. Gdyby wszystkie dopuszczalne uporządkowania miały punkty sekwencji między modyfikacją ++ia odczytem z iprawej strony przypisania, kolejność byłaby nieokreślona. Ponieważ jedna z dopuszczalnych kolejności robi te dwie rzeczy bez interweniującego punktu sekwencji, zachowanie jest nieokreślone.
Steve Jessop

1
@Philip: Nie tylko zdefiniowano nieokreślone zachowanie jako niezdefiniowane zachowanie. Ponownie, jeśli zakres nieokreślonych zachowań obejmuje takie, które są nieokreślone, to ogólne zachowanie jest niezdefiniowane. Jeśli zakres nieokreślonego zachowania jest zdefiniowany we wszystkich możliwościach, to ogólne zachowanie jest nieokreślone. Ale masz rację w drugiej kwestii, myślałem o zdefiniowanym przez użytkownika ai wbudowanym i.
Steve Jessop,

8

Myślę, że to dobrze zdefiniowane:

Z wersji roboczej standardu C ++ (n1905) § 1.9 / 16:

„Istnieje również punkt sekwencji po skopiowaniu zwracanej wartości i przed wykonaniem jakichkolwiek wyrażeń poza funkcją13). Kilka kontekstów w C ++ powoduje ocenę wywołania funkcji, mimo że w jednostce translacji nie pojawia się odpowiednia składnia wywołania funkcji. [ Przykład : ocena nowego wyrażenia wywołuje jedną lub więcej funkcji alokacji i konstruktora; patrz 5.3.4. Dla innego przykładu, wywołanie funkcji konwersji (12.3.2) może wystąpić w kontekstach, w których nie pojawia się żadna składnia wywołania funkcji. - end przykład ] Sekwencja punktów na wejściu i wyjściu z funkcji (jak opisano powyżej) to cechy wywołań funkcji zgodnie z oceną, niezależnie od składni wyrażeniaktóra wywołuje funkcję może być. "

Zwróć uwagę na część, którą pogrubiłem. Oznacza to, że rzeczywiście istnieje punkt sekwencji po wywołaniu funkcji inkrementacji ( i.operator ++()), ale przed wywołaniem przypisania złożonego ( i.operator+=).


6

W porządku. Po przejrzeniu poprzednich odpowiedzi, ponownie przemyślałem własne pytanie, szczególnie tę część, na którą tylko Noah próbował odpowiedzieć, ale nie jestem do niego całkowicie przekonany.

a[++i] = i;

Przypadek 1:

Jeśli ajest tablicą typu wbudowanego. Zatem to, co powiedział Noe, jest poprawne. To jest,

a [++ i] = i nie ma tyle szczęścia, jeśli zakładasz, że a to Twój podstawowy typ tablicy, lub nawet zdefiniowany przez użytkownika . Problem polega na tym, że nie wiemy, która część wyrażenia zawierającego i jest oceniana jako pierwsza.

Tak więc a[++i]=iwywołuje niezdefiniowane zachowanie lub wynik jest nieokreślony. Cokolwiek to jest, nie jest dobrze zdefiniowane!

PS: W powyższym cytacie, przekreślenie jest oczywiście mój.

Przypadek 2:

Jeśli ajest obiektem typu zdefiniowanego przez użytkownika, który przeciąża element operator[], to znowu istnieją dwa przypadki.

  1. Jeśli typ zwracany przez przeciążoną operator[]funkcję jest typem wbudowanym, wówczas ponownie a[++i]=iwywołuje niezdefiniowane zachowanie lub wynik jest nieokreślony.
  2. Ale jeśli typ zwracany przez przeciążoną operator[]funkcję jest typem zdefiniowanym przez użytkownika, to zachowanie a[++i] = ijest dobrze zdefiniowane (o ile rozumiem), ponieważ w tym przypadku a[++i]=ijest równoważne pisaniu, a.operator[](++i).operator=(i);które jest takie samo jak a[++i].operator=(i);,. Oznacza to, że przypisanie operator=zostaje wywołane na zwrócony obiekt of a[++i], który wydaje się być bardzo dobrze zdefiniowany, ponieważ do czasu a[++i]powrotu ++izostały już ocenione, a następnie zwrócony obiekt wywołuje operator=funkcję przekazującą jej zaktualizowaną wartość ijako argument. Zwróć uwagę, że między tymi dwoma wywołaniami istnieje punkt sekwencji . A składnia zapewnia, że ​​nie ma konkurencji między tymi dwoma wywołaniami, aoperator[]byłby wywoływany jako pierwszy, a następnie argument ++iprzekazany do niego również zostałby oceniony jako pierwszy.

Pomyśl o tym, jak o tym, że someInstance.Fun(++k).Gun(10).Sun(k).Tun();każde kolejne wywołanie funkcji zwraca obiekt pewnego typu zdefiniowanego przez użytkownika. Dla mnie ta sytuacja wygląda bardziej tak: eat(++k);drink(10);sleep(k)ponieważ w obu sytuacjach istnieje punkt sekwencji po każdym wywołaniu funkcji.

Proszę popraw mnie jeżeli się mylę. :-)


1
@Nawaz k++i niek są oddzielone punktami sekwencji. Oba mogą być ocenione przed jedną lub z nich. Język wymaga tylko, aby był oceniany wcześniej , a nie argumenty tego są oceniane przed argumentami. W pewnym sensie wyjaśniam to samo ponownie, nie będąc w stanie podać odniesienia, więc nie będziemy robić postępów w tym miejscu. SunFunFunSunFunSun
Philip Potter

1
@Nawaz: ponieważ nie ma nic, co definiuje oddzielający je punkt sekwencji. Istnieją punkty sekwencji przed i po Sunwykonaniu, ale Funargument ++kmoże zostać oceniony przed lub po nim. Istnieją punkty sekwencji przed i po Funwykonaniu, ale Sunargument kmoże zostać oceniony przed lub po nim. Dlatego jednym możliwym przypadkiem jest to, że oba ki ++ksą oceniane przed albo Sunlub Funsą oceniane, a więc oba znajdują się przed punktami sekwencji wywołania funkcji, więc nie ma oddzielającego punktu sekwencji ki ++k.
Philip Potter

1
@Philip: Powtarzam: czym różni się ta sytuacja eat(i++);drink(10);sleep(i);? ... nawet teraz, można powiedzieć, że i++może zostać ocenione przed czy po tym?
Nawaz

1
@Nawaz: jak mogę wyrazić się bardziej jasno? W przykładzie Fun / Sun nie ma punktu sekwencji między ka ++k. W przykładzie jedzenia / picia, nie jest jako punkt sekwencji pomiędzy ii i++.
Philip Potter

3
@Philip: to w ogóle nie ma sensu. Pomiędzy Fun () i Sun () istnieje punkt sekwencji, ale między ich argumentem nie ma punktów sekwencji. To tak, jakby powiedzieć, pomiędzy eat()i sleep()istnieje punkt (y) sekwencji, ale między nimi nie ma nawet jednego. W jaki sposób argumenty dwóch wywołań funkcji oddzielone punktami sekwencji mogą należeć do tych samych punktów sekwencji?
Nawaz
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.