Kolejność wykonywania C ++ w łańcuchu metod

108

Wynik tego programu:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Jest:

method 1
method 2:0

Dlaczego nunie ma 1, kiedy meth2()zaczyna się?

Moises Viñas
źródło
42
@MartinBonner: Chociaż znam odpowiedź, nie nazwałbym tego „oczywistym” w żadnym znaczeniu tego słowa, a nawet gdyby tak było, nie byłby to przyzwoity powód, by przegłosować. Niezadowalający!
Wyścigi lekkości na orbicie
4
Oto, co otrzymujesz, modyfikując swoje argumenty. Funkcje modyfikujące swoje argumenty są trudniejsze do odczytania, ich efekty są nieoczekiwane dla następnego programisty pracującego nad kodem i prowadzą do takich niespodzianek. Zdecydowanie radzę unikać modyfikowania jakichkolwiek parametrów poza inwokantem. Modyfikacja wywołującego nie byłaby tutaj problemem, ponieważ druga metoda jest wywoływana na wyniku pierwszej, więc efekty są na niej uporządkowane. Nadal są jednak przypadki, w których nie byłoby.
Jan Hudec
@JanHudec Właśnie dlatego programowanie funkcjonalne kładzie tak duży nacisk na czystość funkcji.
Pharap
2
Na przykład, konwencja wywoływania oparta na stosie prawdopodobnie wolałaby odepchnąć nu, &nua następnie cna stos w tej kolejności, następnie wywołać meth1, odłożyć wynik na stos, a następnie wywołać meth2, podczas gdy konwencja wywoływania oparta na rejestrze chciałaby załaduj ci &nudo rejestrów, wywołaj meth1, załaduj nudo rejestru, a następnie wywołaj meth2.
Neil

Odpowiedzi:

66

Ponieważ kolejność oceny jest nieokreślona.

Widzisz nuw mainocenianej na 0zanim nawet meth1nazywa. To jest problem z łańcuchem. Radzę tego nie robić.

Po prostu utwórz ładny, prosty, przejrzysty, czytelny i łatwy do zrozumienia program:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Wyścigi lekkości na orbicie
źródło
14
Istnieje możliwość, że propozycja wyjaśnienia kolejności ewaluacji w niektórych przypadkach , która rozwiązuje ten problem, pojawi się w C ++ 17
Revolver_Ocelot
7
Lubię tworzenie łańcuchów metod (np. <<Dla danych wyjściowych i "konstruktorów obiektów" dla złożonych obiektów ze zbyt dużą liczbą argumentów dla konstruktorów - ale bardzo źle się miesza z argumentami wyjściowymi).
Martin Bonner wspiera Monikę
34
Czy dobrze to rozumiem? kolejność oceny meth1i meth2jest zdefiniowana, ale ocena parametru dla meth2może nastąpić przed meth1wywołaniem ...?
Roddy
7
Łączenie metod jest w porządku, o ile metody są rozsądne i modyfikują tylko wywołanie (dla których efekty są dobrze uporządkowane, ponieważ druga metoda jest wywoływana na wyniku pierwszej).
Jan Hudec
4
To logiczne, kiedy się nad tym zastanowić. Działa tak, jakmeth2(meth1(c, &nu), nu)
BartekChom
29

Myślę, że ta część projektu normy dotycząca kolejności ocen jest istotna:

1.9 Wykonanie programu

...

  1. O ile nie zaznaczono inaczej, oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń nie są sekwencjonowane. Obliczenia wartości operandów operatora są sekwencjonowane przed obliczeniem wartości wyniku operatora. Jeśli efekt uboczny na obiekcie skalarnym nie jest sekwencjonowany względem innego efektu ubocznego tego samego obiektu skalarnego lub obliczenia wartości przy użyciu wartości tego samego obiektu skalarnego i nie są one potencjalnie współbieżne, zachowanie jest niezdefiniowane

i również:

5.2.2 Wywołanie funkcji

...

  1. [Uwaga: Obliczenia wyrażenia z przyrostkiem i argumentów nie mają kolejności względem siebie. Wszystkie skutki uboczne oceny argumentów są sekwencjonowane przed wejściem do funkcji - uwaga końcowa]

Więc dla twojej linii c.meth1(&nu).meth2(nu);zastanów się, co dzieje się w operatorze pod względem operatora wywołania funkcji dla końcowego wywołania meth2, więc wyraźnie widzimy podział na wyrażenie i argument z przyrostka nu:

operator()(c.meth1(&nu).meth2, nu);

Wartości wyrażenia przyrostka i argumentu dla końcowego wywołania funkcji (tj. Wyrażenia przyrostka c.meth1(&nu).meth2i nu) nie są sekwencjonowane względem siebie, zgodnie z powyższą regułą wywołania funkcji . Dlatego efekt uboczny obliczania wyrażenia postfiksowego na obiekcie skalarnym arjest bez kolejności w stosunku do oceny argumentu nuprzed meth2wywołaniem funkcji. Zgodnie z powyższą regułą wykonywania programu jest to niezdefiniowane zachowanie.

Innymi słowy, kompilator nie musi oceniać nuargumentu meth2wywołania po meth1wywołaniu - może zakładać, że nie ma skutków ubocznych meth1wpływania na nuocenę.

Kod asemblera utworzony przez powyższe zawiera następującą sekwencję w mainfunkcji:

  1. Zmienna nujest przydzielana na stosie i inicjalizowana z wartością 0.
  2. Rejestr ( ebxw moim przypadku) otrzymuje kopię wartościnu
  3. Adresy nui csą ładowane do rejestrów parametrów
  4. meth1 jest nazywany
  5. Rejestr wartość powrotu a poprzednio buforowane wartość z nuw ebxrejestrze są ładowane do rejestrów parametrów
  6. meth2 jest nazywany

Co najważniejsze, w kroku 5 powyżej kompilator umożliwia nuponowne użycie zbuforowanej wartości z kroku 2 w wywołaniu funkcji meth2. W tym przypadku pomija możliwość, która numogła zostać zmieniona przez wezwanie do meth1- „niezdefiniowanego zachowania” w akcji.

UWAGA: Ta odpowiedź zmieniła się zasadniczo w stosunku do pierwotnej formy. Moje początkowe wyjaśnienie w zakresie skutków ubocznych obliczania argumentów, które nie były sekwencjonowane przed ostatecznym wywołaniem funkcji, było niepoprawne, ponieważ tak jest. Problem polega na tym, że obliczenia samych operandów są nieokreślone.

Smeeheey
źródło
2
To jest źle. Wywołania funkcji są sekwencjonowane w nieokreślony sposób w / r / t innych wartości w funkcji wywołującej (chyba, że ​​ograniczenie sekwencjonowane przed narzuca się inaczej); nie przeplatają się.
TC
1
@TC - nigdy nie powiedziałem nic o przeplataniu wywołań funkcji. Odniosłem się tylko do skutków ubocznych operatorów. Jeśli spojrzysz na kod asemblera utworzony przez powyższe, zobaczysz, że meth1jest wykonywany wcześniej meth2, ale parametr for meth2jest wartością nubuforowaną w rejestrze przed wywołaniem meth1- tj. Kompilator zignorował potencjalne skutki uboczne, którymi są zgodne z moją odpowiedzią.
Smeeheey
1
Twierdzisz dokładnie, że - „jego efekt uboczny (tj. Ustawienie wartości ar) nie jest gwarantowany w kolejności przed wywołaniem”. Ocena wyrażenia postfiksowego w wywołaniu funkcji (którym jest c.meth1(&nu).meth2) i ocena argumentu tego wywołania ( nu) są generalnie bez kolejności, ale 1) ich skutki uboczne są sekwencjonowane przed wejściem meth2i 2) ponieważ c.meth1(&nu)jest wywołaniem funkcji , jest nieokreślony sekwencjonowany z oceną nu. Wewnątrz meth2, gdyby w jakiś sposób uzyskał wskaźnik do zmiennej w main, zawsze zobaczyłby 1.
TC
2
„Jednak efekt uboczny obliczania operandów (tj. Ustawianie wartości ar) nie jest gwarantowany w kolejności przed czymkolwiek (jak w 2) powyżej”. Jest absolutnie gwarantowane, że zostanie ono ustawione w kolejności przed wywołaniem meth2, jak wspomniano w punkcie 3 strony cppreference, którą cytujesz (którą również pominąłeś we właściwy sposób zacytować).
TC
1
Zrobiłeś coś złego i pogorszyłeś sprawę. Nie ma tu absolutnie żadnego niezdefiniowanego zachowania. Czytaj dalej [intro.execution] / 15, poza przykładem.
TC
9

W standardzie C ++ z 1998 r., Sekcja 5, pkt 4

O ile nie zaznaczono, kolejność oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń oraz kolejność, w jakiej występują skutki uboczne, jest nieokreślona. Pomiędzy poprzednim a następnym punktem sekwencji obiekt skalarny będzie miał swoją przechowywaną wartość zmodyfikowaną co najmniej raz przez ocenę wyrażenia. Ponadto dostęp do wcześniejszej wartości jest możliwy tylko w celu określenia wartości, która ma być przechowywana. Wymagania niniejszego punktu muszą być spełnione dla każdego dopuszczalnego uporządkowania podwyrażeń pełnego wyrażenia; w przeciwnym razie zachowanie jest niezdefiniowane.

(Pominąłem odniesienie do przypisu # 53, który nie dotyczy tego pytania).

Zasadniczo &nunależy ocenić przed wywołaniem c1::meth1()i nuprzed wywołaniem c1::meth2(). Nie ma jednak żadnego wymagania, które nunależy ocenić wcześniej &nu(np. Dozwolone jest, aby nunajpierw zostać ocenione &nu, a następnie c1::meth1()wywoływane - co może być tym, co robi twój kompilator). Wyrażenie *ar = 1w c1::meth1()związku z tym nie jest gwarantowana być oceniane przed nuw main()oceniana jest, aby być przekazywane c1::meth2().

Późniejsze standardy C ++ (których obecnie nie mam na komputerze, którego używam dziś wieczorem) mają zasadniczo tę samą klauzulę.

Piotr
źródło
7

Myślę, że podczas kompilacji, zanim naprawdę zostaną wywołane funkcje met1 i met2, parametry zostały do ​​nich przekazane. Mam na myśli, kiedy używasz „c.meth1 (& nu) .meth2 (nu);” wartość nu = 0 została przekazana do meth2, więc nie ma znaczenia, czy "nu" zostanie zmienione później.

możesz spróbować tego:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

otrzyma odpowiedź, której chcesz

Koszulka Saintor
źródło