Czy operatory logiczne zwierające są wymagane? A kolejność wyceny?

140

Czy norma ANSI nakazuje zwarcie operatorów logicznych w języku C lub C ++?

Jestem zdezorientowany, ponieważ przypominam sobie książkę K&R, w której napisano, że twój kod nie powinien polegać na zwarciu tych operacji, ponieważ mogą nie. Czy ktoś mógłby wskazać, gdzie w standardzie mówi się, że operacje logiczne są zawsze zwarte? Najbardziej interesuje mnie C ++, odpowiedź również na C byłaby świetna.

Pamiętam również, że przeczytałem (nie pamiętam gdzie), że kolejność oceny nie jest ściśle zdefiniowana, więc Twój kod nie powinien zależeć ani zakładać, że funkcje w wyrażeniu byłyby wykonywane w określonej kolejności: na końcu instrukcji wszystkie funkcje, do których się odwołujesz zostanie wywołana, ale kompilator ma swobodę wyboru najbardziej wydajnej kolejności.

Czy norma wskazuje kolejność oceny tego wyrażenia?

if( functionA() && functionB() && functionC() ) cout<<"Hello world";
Joe Pineda
źródło
12
Ostrożnie: dotyczy to typów POD. Ale jeśli przeciążasz operator && lub operator || dla określonej klasy NIE są to powtarzam NIE skrót. Dlatego zaleca się, aby NIE definiować tych operatorów dla własnych klas.
Martin York,
Przedefiniowałem te operatory jakiś czas temu, kiedy stworzyłem klasę, która wykonywałaby podstawowe operacje algebry boolowskiej. Prawdopodobnie powinien umieścić komentarz ostrzegawczy "to niszczy zwarcia i ocenę lewy-prawy!" na wypadek, gdybym o tym zapomniał. Również przeładowany * / + i uczynił z nich synonimy :-)
Joe Pineda
Posiadanie wywołań funkcji w bloku if nie jest dobrą praktyką programistyczną. Zawsze miej zadeklarowaną zmienną, która przechowuje wartość zwracaną metody i używaj jej w bloku if.
SR Chaitanya,
6
@SRChaitanya To nie jest poprawne. To, co arbitralnie określasz jako kiepską praktykę, jest wykonywane przez cały czas, szczególnie w przypadku funkcji, które zwracają wartości logiczne, jak tutaj.
Markiz Lorne,

Odpowiedzi:

154

Tak, kolejność zwarć i oceny jest wymagana dla operatorów ||i &&zarówno w standardach C, jak i C ++.

Standard C ++ mówi (w standardzie C powinna być klauzula równoważna):

1.9.18

Podczas oceny następujących wyrażeń

a && b
a || b
a ? b : c
a , b

używając wbudowanego znaczenia operatorów w tych wyrażeniach, po ocenie pierwszego wyrażenia (12) znajduje się punkt sekwencji .

W C ++ istnieje dodatkowa pułapka: zwarcie NIE ma zastosowania do typów, które przeciążają operatory ||i &&.

Przypis 12: Operatory wskazane w tym akapicie są operatorami wbudowanymi, jak opisano w klauzuli 5. Gdy jeden z tych operatorów jest przeciążony (klauzula 13) w prawidłowym kontekście, wyznaczając w ten sposób funkcję operatora zdefiniowaną przez użytkownika, wyrażenie oznacza wywołanie funkcji, a operandy tworzą listę argumentów bez implikowanego punktu sekwencji między nimi.

Zwykle nie zaleca się przeciążania tych operatorów w C ++, chyba że masz bardzo szczegółowe wymagania. Możesz to zrobić, ale może to zepsuć oczekiwane zachowanie w kodzie innych osób, zwłaszcza jeśli te operatory są używane pośrednio przez tworzenie wystąpień szablonów z typem przeciążającym te operatory.

Alex B.
źródło
3
Nie wiedziałem, że zwarcie nie będzie miało zastosowania do przeciążonych operacji logicznych, to jest intrygujące. Czy możesz dodać odniesienie do normy lub źródła? Nie ufam ci, po prostu chcę dowiedzieć się więcej na ten temat.
Joe Pineda,
4
tak, to logiczne. działa jako argument dla operatora && (a, b). to jego realizacja mówi, co się dzieje.
Johannes Schaub - litb
10
litb: Po prostu nie jest możliwe przekazanie b do operatora && (a, b) bez jego oceny. I nie ma możliwości cofnięcia oceny b, ponieważ kompilator nie może zagwarantować, że nie będzie żadnych skutków ubocznych.
jmucchiello
2
Uważam to za smutne. Pomyślałbym, że powinienem przedefiniować operatory && i || i nadal są w pełni deterministyczne , kompilator wykryłby to i ograniczyłby ich ocenę: w końcu porządek jest nieistotny i gwarantują brak skutków ubocznych!
Joe Pineda
2
@Joe: ale zwracana wartość i argumenty operatora mogą zmienić się z wartości logicznej na coś innego. Kiedyś implementowałem "specjalną" logikę z TRZY wartościami ("prawda", "fałsz" i "nieznane"). Wartość zwracana jest deterministyczna, ale zachowanie polegające na zwarciu nie jest właściwe.
Alex B
70

Ocena zwarcia i kolejność oceny to obowiązkowy standard semantyczny zarówno w C, jak i C ++.

Gdyby tak nie było, taki kod nie byłby powszechnym idiomem

   char* pChar = 0;
   // some actions which may or may not set pChar to something
   if ((pChar != 0) && (*pChar != '\0')) {
      // do something useful

   }

Sekcja 6.5.13 Operator logiczny AND specyfikacji C99 (łącze PDF) mówi

(4). W przeciwieństwie do bitowego operatora binarnego &, operator && gwarantuje ocenę od lewej do prawej; po ocenie pierwszego argumentu znajduje się punkt sekwencji. Jeśli pierwszy operand jest porównywany jako równy 0, drugi operand nie jest oceniany.

Podobnie mówi sekcja 6.5.14 Operator logiczny OR

(4) W przeciwieństwie do bitowego | operator, || operator gwarantuje ocenę od lewej do prawej; po ocenie pierwszego argumentu znajduje się punkt sekwencji. Jeśli pierwszy operand porównuje nierówność z 0, drugi operand nie jest oceniany.

Podobne sformułowania można znaleźć w standardach C ++, patrz sekcja 5.14 w tej wersji roboczej . Jak zaznacza warcaby w innej odpowiedzi, jeśli przesłonisz && lub ||, to oba operandy muszą zostać ocenione, ponieważ stanie się zwykłym wywołaniem funkcji.

Paul Dixon
źródło
Ach, czego szukałem! OK, więc zarówno kolejność oceny, jak i zwarcie są wymagane zgodnie z ANSI-C 99! Naprawdę chciałbym zobaczyć równoważne odniesienie dla ANSI-C ++, chociaż jestem prawie w 99%, to musi być to samo.
Joe Pineda,
Trudno znaleźć dobry darmowy link do standardów C ++, podlinkowałem do wersji roboczej, którą znalazłem z niektórymi googlami.
Paul Dixon
Prawda dla typów POD. Ale jeśli przeciążasz operator && lub operator || to nie są skróty.
Martin York,
1
tak, warto zauważyć, że w przypadku bool zawsze będziesz mieć gwarantowaną kolejność oceny i zachowanie zwarcia. ponieważ nie można przeciążać operatora && dla dwóch typów wbudowanych. potrzebujesz co najmniej jednego typu zdefiniowanego przez użytkownika w operandach, aby zachowywał się inaczej.
Johannes Schaub - litb
Chciałbym móc zaakceptować oba Warcaby i tę odpowiedź. Ponieważ najbardziej interesuje mnie C ++, akceptuję ten drugi, choć muszę przyznać, że jest on też wspaniały! Dziękuję Ci bardzo!
Joe Pineda
19

Tak, nakazuje to (zarówno kolejność oceny, jak i zwarcie). W naszym przykładzie, jeśli wszystkie funkcje zwracają wartość true, kolejność wywołań pochodzi ściśle od functionA, następnie functionB, a następnie functionC. Używany do tego jak

if(ptr && ptr->value) { 
    ...
}

To samo dotyczy operatora przecinka:

// calls a, then b and evaluates to the value returned by b
// which is used to initialize c
int c = (a(), b()); 

Mówi się od lewego i prawego argumentu instrukcji &&, ||, ,a pomiędzy pierwszą i drugą / trzecią operandu ?:(operator warunkowych) stanowi „punkt sekwencji”. Wszelkie skutki uboczne są oceniane całkowicie przed tym punktem. Więc to jest bezpieczne:

int a = 0;
int b = (a++, a); // b initialized with 1, and a is 1

Zauważ, że operatora przecinka nie należy mylić z przecinkiem składniowym używanym do oddzielania rzeczy:

// order of calls to a and b is unspecified!
function(a(), b());

Standard C ++ mówi w 5.14/1:

Operator && grupuje od lewej do prawej. Operandy są niejawnie konwertowane na typ bool (klauzula 4). Wynik jest prawdziwy, jeśli oba operandy są prawdziwe i fałszywe w przeciwnym razie. W przeciwieństwie do &, && gwarantuje ocenę od lewej do prawej: drugi argument nie jest oceniany, jeśli pierwszy operand jest fałszywy.

A w 5.15/1:

|| grupy operatorów od lewej do prawej. Oba operandy są niejawnie konwertowane na bool (klauzula 4). Zwraca prawdę, jeśli którykolwiek z jej operandów jest prawdziwy, a fałsz w przeciwnym razie. W przeciwieństwie do |, || gwarantuje ocenę od lewej do prawej; co więcej, drugi argument nie jest oceniany, jeśli wynik pierwszego argumentu jest prawdziwy.

Mówi o obu obok tych:

Rezultatem jest bool. Wszystkie skutki uboczne pierwszego wyrażenia z wyjątkiem zniszczenia prowizorycznych (12.2) występują przed oceną drugiego wyrażenia.

Oprócz tego, 1.9/18mówi

W ocenie każdego z wyrażeń

  • a && b
  • a || b
  • a ? b : C
  • a , b

Używając wbudowanego znaczenia operatorów w tych wyrażeniach (5.14, 5.15, 5.16, 5.18), po ocenie pierwszego wyrażenia znajduje się punkt sekwencji.

Johannes Schaub - litb
źródło
10

Prosto ze starego dobrego K&R:

C gwarantuje to &&i ||są oceniani od lewej do prawej - wkrótce zobaczymy przypadki, w których ma to znaczenie.

John T.
źródło
3
K&R wydanie 2 s40. „Wyrażenia połączone przez && lub || są oceniane od lewej do prawej, a ocena zatrzymuje się, gdy tylko znana jest prawda lub fałsz wyniku. Większość programów w języku C opiera się na tych właściwościach”. Nie mogę znaleźć Twojego cytowanego tekstu w żadnym miejscu w książce. Czy to z wyjątkowo przestarzałej pierwszej edycji? Proszę wyjaśnić, gdzie znalazłeś ten tekst.
Lundin
1
Okazuje się, że cytujesz ten starożytny samouczek . Pochodzi z 1974 roku i jest wysoce nieistotny.
Lundin
6

Bądź bardzo ostrożny.

W przypadku typów podstawowych są to operatory skrótów.

Ale jeśli zdefiniujesz te operatory dla własnej klasy lub typów wyliczeń, nie będą one skrótami. Z powodu tej semantycznej różnicy w ich użyciu w tych różnych okolicznościach nie zaleca się definiowania tych operatorów.

Dla typów podstawowych operator &&i operator ||dla typów podstawowych kolejność oceny jest od lewej do prawej (w przeciwnym razie krótkie cięcie byłoby trudne :-). Ale w przypadku przeciążonych operatorów, które definiujesz, są to w zasadzie cukier syntaktyczny do zdefiniowania metody, a zatem kolejność oceny parametrów jest nieokreślony.

Martin York
źródło
1
Przeciążenie operatora nie ma nic wspólnego z typem będącym POD lub nie. Aby zdefiniować funkcję operatora, co najmniej jeden z argumentów musi być klasą (lub strukturą lub sumą), wyliczeniem lub odwołaniem do jednego z nich. Bycie POD oznacza, że ​​możesz używać na nim memcpy.
Derek Ledbetter
I to właśnie mówiłem. Jeśli przeciążasz && dla swojej klasy, to tak naprawdę jest to tylko wywołanie metody. Nie można więc polegać na kolejności oceny parametrów. Oczywiście nie możesz przeciążać && dla typów POD.
Martin York,
3
Nieprawidłowo używasz terminu „typy POD”. Możesz przeciążać && dla dowolnej struktury, klasy, unii lub wyliczenia, POD lub nie. Nie można przeciążać &&, jeśli obie strony są typami liczbowymi lub wskaźnikami.
Derek Ledbetter
Używałem POD jako (char / int / float itp.), A nie jako aggrigate POD (o czym mówisz) i zwykle odnosi się to osobno lub bardziej wyraźnie, ponieważ nie jest to typ wbudowany.
Martin York
2
Więc miałeś na myśli „podstawowe typy”, ale napisałeś „typy POD”?
Öö Tiib
0

Twoje pytanie sprowadza się do pierwszeństwa i skojarzenia operatorów C ++ . Zasadniczo w wyrażeniach z wieloma operatorami i bez nawiasów kompilator konstruuje drzewo wyrażeń, postępując zgodnie z tymi regułami.

Aby uzyskać pierwszeństwo, jeśli masz coś takiego A op1 B op2 C, możesz pogrupować rzeczy jako albo (A op1 B) op2 Calbo A op1 (B op2 C). Jeśli op1ma wyższy priorytet niż op2, otrzymasz pierwsze wyrażenie. W przeciwnym razie zdobędziesz drugi.

W przypadku asocjatywności, gdy masz coś podobnego A op B op C, możesz ponownie zgrupować cienkie litery jako (A op B) op Club A op (B op C). Jeśli opopuścił łączność, otrzymujemy pierwsze wyrażenie. Jeśli ma właściwą łączność, kończymy z drugim. Działa to również w przypadku operatorów na tym samym poziomie pierwszeństwa.

W tym konkretnym przypadku &&ma wyższy priorytet niż ||, więc wyrażenie zostanie ocenione jako (a != "" && it == seqMap.end()) || isEven.

Sama kolejność jest „od lewej do prawej” w postaci drzewa wyrażeń. Więc najpierw dokonamy oceny a != "" && it == seqMap.end(). Jeśli to prawda, całe wyrażenie jest prawdziwe, w przeciwnym razie przejdziemy do isEven. Procedura ta oczywiście powtarza się rekurencyjnie wewnątrz lewego podwyrażenia.


Ciekawostki, ale pojęcie pierwszeństwa ma swoje korzenie w notacji matematycznej. To samo dzieje się w a*b + cprzypadku, gdy *ma wyższy priorytet niż +.

Jeszcze bardziej interesujące / niejasne, dla nieprecyzyjnego wyrażenia A1 op1 A2 op2 ... opn-1 An, w którym wszystkie operatory mają ten sam priorytet, liczba drzew wyrażeń binarnych, które moglibyśmy utworzyć, jest określona przez tak zwane liczby katalońskie . W przypadku dużych nrosną one niezwykle szybko. re

Horia Coman
źródło
Wszystko to jest poprawne, ale chodzi o pierwszeństwo operatorów i asocjatywność, a nie o kolejność oceny i ograniczanie ograniczeń. To są różne rzeczy.
Thomas Padron-McCarthy
0

Jeśli ufasz Wikipedii:

[ &&i ||] są semantycznie różne od operatorów bitowych & i | ponieważ nigdy nie ocenią właściwego operandu, jeśli wynik można określić tylko z lewej strony

C (język programowania)

Sophie Alpert
źródło
11
Po co ufać wiki, skoro mamy standard!
Martin York,
1
Jeśli ufasz Wikipedii, „Wikipedia nie jest wiarygodnym źródłem informacji” .
Markiz Lorne,
To prawda, ale niekompletne, ponieważ przeciążone operatory w C ++ nie ulegają zwarciu.
Thomas Padron-McCarthy