Krótkie zachowanie operatorów &&
i ||
jest niesamowitym narzędziem dla programistów.
Ale dlaczego tracą to zachowanie, gdy są przeciążone? Rozumiem, że operatory są jedynie cukrem syntaktycznym dla funkcji, ale operatory dla bool
mają takie zachowanie, dlaczego miałoby być ograniczone do tego jednego typu? Czy jest za tym jakieś techniczne uzasadnienie?
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
{true, false, nil}
. Ponieważnil&& x == nil
może to spowodować zwarcie.std::valarray<bool> a, b, c;
, jak wyobrażasz sobiea || b || c
zwarcie?operator&&
luboperator||
i zależy od obu ocenianych operandów. Zachowanie kompatybilności wstecznej jest (lub powinno być) ważne podczas dodawania funkcji do istniejącego języka.Odpowiedzi:
Wszystkie procesy projektowe prowadzą do kompromisów między wzajemnie niekompatybilnymi celami. Niestety, proces projektowania przeciążonego
&&
operatora w C ++ dał mylący wynik końcowy:&&
pomijano tę samą funkcję, której oczekujesz - jej działanie zwarciowe.Nie znam szczegółów, jak ten proces projektowania skończył się w tym niefortunnym miejscu. Ważne jest jednak, aby zobaczyć, jak późniejszy proces projektowania wziął pod uwagę ten nieprzyjemny wynik. W języku C # przeciążony
&&
operator powoduje zwarcie. Jak projektanci C # to osiągnęli?Jedna z pozostałych odpowiedzi sugeruje „podnoszenie lambda”. To jest:
mogłoby być postrzegane jako coś moralnego odpowiednika:
gdzie drugi argument używa pewnego mechanizmu leniwego oceniania, tak że po oszacowaniu powstają skutki uboczne i wartość wyrażenia. Implementacja przeciążonego operatora spowoduje wykonanie leniwej oceny tylko wtedy, gdy jest to konieczne.
To nie jest to, co zrobił zespół projektowy C #. (Na marginesie: chociaż podnoszenie lambda jest tym, co zrobiłem, gdy przyszedł czas na przedstawienie drzewa wyrażeń tego
??
operatora, który wymaga pewnych operacji konwersji być wykonywane leniwie Opisując to w szczegółach będzie jednak głównym dygresja wystarczy powiedzieć:.. Lambda podnoszenia działa, ale jest na tyle ciężki, że chcieliśmy tego uniknąć).Zamiast tego, rozwiązanie C # dzieli problem na dwa oddzielne problemy:
Dlatego problem rozwiązuje się, uniemożliwiając
&&
bezpośrednie przeciążanie . Zamiast tego w C # należy przeciążać dwa operatory, z których każdy odpowiada na jedno z tych dwóch pytań.(Na marginesie: właściwie trzy. C # wymaga tego if operator
false
podano operator, to operatortrue
należy również podać , co odpowiada na pytanie: czy to jest „prawda-prawda?”. Zwykle nie byłoby powodu, aby podawać tylko jeden taki operator, więc C # wymaga obu.)Rozważ oświadczenie w formie:
Kompilator generuje kod w tym celu, tak jak myślałeś, że napisałeś ten pseudo-C #:
Jak widać, zawsze oceniana jest lewa strona. Jeśli okaże się, że jest to „fałszywe”, to jest to wynik. W przeciwnym razie oceniana jest prawa strona i chętny operator zdefiniowany przez użytkownika
&
wywoływany jest .||
Operator jest zdefiniowana w sposób analogiczny, jak wywołanie operatora prawdziwym i chętnie|
operatora:Poprzez zdefiniowanie wszystkich czterech operatorów -
true
,false
,&
i|
- C # pozwala nie tylko mówią,cleft && cright
ale też nie zwarciecleft & cright
, a takżeif (cleft) if (cright) ...
, ac ? consequence : alternative
iwhile(c)
, i tak dalej.Teraz powiedziałem, że wszystkie procesy projektowe są wynikiem kompromisu. Tutaj projektantom języka C # udało się uzyskać zwarcie
&&
i||
poprawnie, ale zrobienie tego wymaga przeciążenia czterech operatorów zamiast dwóch , co niektórzy uważają za mylące. Operator true / false funkcja jest jedną z najmniej zrozumiałych funkcji w języku C #. Celowi posiadania rozsądnego i prostego języka, który jest znany użytkownikom C ++, sprzeciwiły się pragnienia tworzenia krótkich obwodów i chęć nie implementowania podnoszenia lambda lub innych form leniwej oceny. Myślę, że było to rozsądne stanowisko kompromisowe, ale ważne jest, aby zdać sobie sprawę, że jest to to stanowisko kompromisowe. Po prostu inny pozycja kompromisowa , na którą zdecydowali się projektanci C ++.Jeśli temat projektowania języka dla takich operatorów Cię interesuje, rozważ przeczytanie mojej serii na temat tego, dlaczego C # nie definiuje tych operatorów na wartościach logicznych dopuszczających wartość zerową:
http://ericlippert.com/2012/03/26/null-is-not-false-part-one/
źródło
your post
to nieistotne.His noticing your distinct writing style
nie ma znaczenia.bool
, możesz użyć&&
i||
bez implementacjioperator true/false
luboperator &/|
w C # nie ma problemu. Problem pojawia się właśnie w sytuacji, gdy nie ma konwersji nabool
możliwe lub gdy nie jest się pożądanym.Chodzi o to, że (w granicach C ++ 98) prawostronny operand zostałby przekazany do przeciążonej funkcji operatora jako argument. W ten sposób zostałby już oceniony . Nie ma nic
operator||()
luboperator&&()
kod mógłby lub nie mógł zrobić, co pozwoliłoby uniknąć tego.Oryginalny operator jest inny, ponieważ nie jest to funkcja, ale zaimplementowana na niższym poziomie języka.
Dodatkowe cechy języka mogły sprawić, że składniowo możliwa była brak oceny operandu po prawej stronie . Jednak nie zawracali sobie głowy, ponieważ jest tylko kilka wybranych przypadków, w których byłoby to przydatne semantycznie . (Tak jak
? :
, które w ogóle nie jest dostępne do przeciążenia.(Wprowadzenie lambd do standardu zajęło im 16 lat ...)
Jeśli chodzi o użycie semantyczne, rozważ:
Sprowadza się to do:
Zastanów się, co dokładnie chciałbyś zrobić z obiektem B (nieznanego typu) tutaj, poza wywołaniem operatora konwersji do
bool
, i jak ułożyłbyś to w słowa dla definicji języka.A jeśli ty są nazywając konwersję do bool, dobrze ...
robi to samo, czy teraz? Więc po co w ogóle przeciążać?
źródło
export
.)bool
Operator konwersji dla obu klas ma również dostęp do wszystkich zmiennych składowych i działa dobrze z operatorem wbudowanym. Cokolwiek innego, poza konwersją do wartości bool, i tak nie ma semantycznego sensu dla oceny zwarcia! Spróbuj podejść do tego z semantycznego punktu widzenia, a nie syntaktycznego: co chciałbyś osiągnąć, a nie jak byś to zrobił.&
i dlatego i&&
nie są tym samym operatorem. Dzięki za pomoc w zrozumieniu tego.if (x != NULL && x->foo)
wymaga zwarcia, nie dla szybkości, ale dla bezpieczeństwa.Funkcję należy przemyśleć, zaprojektować, zaimplementować, udokumentować i wysłać.
Teraz pomyśleliśmy o tym, zobaczmy, dlaczego może to być teraz łatwe (a wtedy trudne). Pamiętaj również, że ilość zasobów jest ograniczona, więc dodanie ich mogło spowodować posiekanie czegoś innego (Czego chciałbyś z tego zrezygnować?).
Teoretycznie wszyscy operatorzy mogliby pozwolić na zachowanie zwarciowe z tylko jedną „mniejszą” dodatkową cechą języka , od C ++ 11 (kiedy wprowadzono lambdy, 32 lata po rozpoczęciu "C z klasami" w 1979 r., Wciąż szanowana 16 po c ++ 98):
C ++ potrzebowałby tylko sposobu na oznaczenie argumentu jako leniwego ocenianego - ukrytej lambdy - aby uniknąć oceny, dopóki nie będzie to konieczne i dozwolone (spełnione warunki wstępne).
Jak wyglądałaby ta teoretyczna funkcja (pamiętaj, że wszelkie nowe funkcje powinny być powszechnie używane)?
Adnotacja
lazy
zastosowana do argumentu funkcji sprawia, że funkcja jest szablonem oczekującym funktora i sprawia, że kompilator pakuje wyrażenie do funktora:Pod okładką wyglądałoby to tak:
Zwróć szczególną uwagę, że lambda pozostaje ukryta i zostanie wywołana najwyżej raz.
Z tego powodu nie powinno dojść do pogorszenia wydajności , poza zmniejszonymi szansami na wyeliminowanie zwykłego podwyrażenia.
Oprócz złożoności implementacji i złożoności koncepcyjnej (każda funkcja zwiększa oba te elementy, chyba że dostatecznie ułatwia to złożoność w przypadku niektórych innych funkcji), spójrzmy na inną ważną kwestię: kompatybilność wsteczną.
Chociaż ta funkcja języka nie złamałaby żadnego kodu, subtelnie zmieniłaby każdy wykorzystujący ją interfejs API, co oznacza, że jakiekolwiek użycie w istniejących bibliotekach byłoby cichą, przełomową zmianą.
BTW: Ta funkcja, choć łatwiejsza w użyciu, jest silniejsza niż rozwiązanie C # dzielenia
&&
i||
na dwie funkcje, każda dla oddzielnej definicji.źródło
&&
pobieranie jednego argumentu typu „wskaźnik do funkcji zwracającej T” oraz dodatkowej reguły konwersji, która umożliwia niejawną konwersję wyrażenia argumentu typu T na wyrażenie lambda. Zwróć uwagę, że nie jest to zwykła konwersja, ponieważ musi być wykonana na poziomie syntaktycznym: przekształcenie wartości typu T w funkcję w czasie wykonywania nie byłoby przydatne, ponieważ ocena byłaby już wykonana.Z retrospektywną racjonalizacją, głównie dlatego, że
aby mieć zagwarantowane zwarcie (bez wprowadzania nowej składni), operatory musiałyby być ograniczone do
wynikirzeczywisty pierwszy argument, który można zamienić nabool
iW razie potrzeby zwarcie można łatwo wyrazić na inne sposoby.
Na przykład, jeśli klasa
T
ma skojarzone&&
i||
operatory, to wyrażeniegdzie
a
,b
ic
są wyrażeniami typuT
, może być wyrażony jako zwarciea może jaśniej jako
Pozorna nadmiarowość zabezpiecza wszelkie skutki uboczne wywołań operatora.
Podczas gdy przepisywanie lambda jest bardziej szczegółowe, jego lepsze hermetyzacja pozwala na zdefiniowanie takich operatorów.
Nie jestem do końca pewien, czy wszystkie poniższe elementy są zgodne ze standardami (wciąż trochę influensa), ale kompiluje się czysto z Visual C ++ 12.0 (2013) i MinGW g ++ 4.8.2:
Wynik:
Tutaj każdy
!!
bang-bang pokazuje konwersję nabool
, tj. Sprawdzenie wartości argumentów.Ponieważ kompilator może łatwo zrobić to samo i dodatkowo ją zoptymalizować, jest to zademonstrowana możliwa implementacja i każde twierdzenie o niemożliwości należy umieścić w tej samej kategorii, co twierdzenia o niemożliwości w ogóle, a mianowicie ogólnie bzdury.
źródło
&&
- musiałby być dodatkowy wiersz podobny doif (!a) { return some_false_ish_T(); }
- i do pierwszego punktu: w zwarciu chodzi o parametry, które można zamienić na bool, a nie o wyniki.bool
jest konieczna do zrobienia zwarcia.||
ale nie&&
. Drugi komentarz miał na celu „musiałyby być ograniczone do wyników konwertowanych na bool” w pierwszym podpunkcie - powinien brzmieć „ograniczone do parametrów konwertowanych na bool” imo.bool
, aby sprawdzić, czy nie występują krótkie krążenia dalszych operatorów w wyrażeniu. Na przykład wynika && b
musi zostać przekonwertowany na,bool
aby sprawdzić, czy nie występuje krótkie krążenie logicznego LUB wa && b || c
.tl; dr : nie jest to warte wysiłku ze względu na bardzo niskie zapotrzebowanie (kto by używał tej funkcji?) w porównaniu z dość wysokimi kosztami (wymagana specjalna składnia).
Pierwszą rzeczą, która przychodzi na myśl, jest to, że przeciążanie operatorów to po prostu fantazyjny sposób pisania funkcji, podczas gdy logiczna wersja operatorów
||
i&&
to tylko buitlin. Oznacza to, że kompilator możex = y && z
dowolnie je zwierać, podczas gdy wyrażenie ma wartość nonbooleany
iz
musi prowadzić do wywołania funkcji takiej jakX operator&& (Y, Z)
. Oznaczałoby to, żey && z
jest to po prostu fantazyjny sposób pisania,operator&&(y,z)
który jest po prostu wywołaniem dziwnie nazwanej funkcji, w której oba parametry muszą zostać ocenione przed wywołaniem funkcji (w tym wszystko, co uzna za stosowne zwarcie).Można by jednak argumentować, że powinno być możliwe
&&
nieco bardziej wyszukane tłumaczenie operatorów, tak jak ma to miejsce w przypadkunew
operatora, który jest tłumaczony na wywołanie funkcji,operator new
po której następuje wywołanie konstruktora.Z technicznego punktu widzenia nie stanowiłoby to problemu, należałoby zdefiniować składnię języka specyficzną dla warunku wstępnego, który umożliwia tworzenie zwarć. Jednak użycie zwarć byłoby ograniczone do przypadków, w których
Y
jest to możliweX
, albo musiałyby istnieć dodatkowe informacje o tym, jak faktycznie wykonać zwarcie (tj. Obliczyć wynik tylko z pierwszego parametru). Wynik musiałby wyglądać mniej więcej tak:Rzadko chce się przeciążać,
operator||
aoperator&&
ponieważ rzadko zdarza się, że pisaniea && b
jest rzeczywiście intuicyjne w kontekście innym niż boolowski. Jedyne wyjątki, które znam, to szablony wyrażeń, np. Dla wbudowanych DSL. Tylko w kilku z tych kilku przypadków ocena zwarć byłaby korzystna. Szablony wyrażeń zwykle tego nie robią, ponieważ są używane do tworzenia drzew wyrażeń, które są oceniane później, więc zawsze potrzebujesz obu stron wyrażenia.W skrócie: ani autorzy kompilatorów, ani autorzy standardów nie odczuwali potrzeby przeskakiwania przez obręcze oraz definiowania i implementowania dodatkowej, kłopotliwej składni, tylko dlatego, że jeden na milion mógłby pomyśleć, że byłoby miło mieć zwarcie w zdefiniowanym przez użytkownika
operator&&
ioperator||
- po prostu aby dojść do wniosku, że nie jest to mniejszy wysiłek niż napisanie logiki odręcznie.źródło
lazy
które zamieniają wyrażenie podane jako argumenty niejawnie w funkcję anonimową. Daje to wywołanej funkcji wybór, czy wywołać ten argument, czy nie. Więc jeśli język ma już lambdy, potrzebna dodatkowa składnia jest bardzo mała. „Pseudokod”: X i (A a, leniwy B b) {if (cond (a)) {return short (a); } else {faktyczny (a, b ()); }}std::function<B()>
, co spowodowałoby pewien narzut. Lub jeśli chcesz to wbudować, zrób totemplate <class F> X and(A a, F&& f){ ... actual(a,F()) ...}
. I może przeładować go parametrem „normalnym”B
, aby wywołujący mógł zdecydować, którą wersję wybrać.lazy
Składnia może być bardziej wygodne, ale ma pewien kompromis wydajności.std::function
wersją versuslazy
jest to, że pierwszy można ocenić wielokrotnie. Leniwy parametr,foo
który jest używany jakofoo+foo
nadal oceniany tylko raz.X
można je obliczyćY
samodzielnie. Zupełnie inaczej.std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}
. Chyba że używasz bardzo swobodnego użycia terminu „konwersja”.operator&&
ręcznie napisać logikę zwarcia niestandardowego . Pytanie nie brzmi, czy to możliwe, ale dlaczego nie ma krótkiej wygodnej drogi.Lambdy to nie jedyny sposób na wprowadzenie lenistwa. Leniwa ocena jest stosunkowo prosta przy użyciu szablonów wyrażeń w C ++. Nie ma potrzeby używania słowa kluczowego
lazy
i można to zaimplementować w C ++ 98. Drzewa wyrażeń są już wspomniane powyżej. Szablony wyrażeń to słabe (ale sprytne) drzewa ekspresji człowieka. Sztuczka polega na przekształceniu wyrażenia w drzewo rekursywnie zagnieżdżonych instancjiExpr
szablonu. Drzewo jest oceniane oddzielnie po zakończeniu budowy.Poniższy kod implementuje skróty
&&
i||
operatory dla klasy,S
o ile zapewnialogical_and
ilogical_or
wolne funkcje i można go przekonwertować nabool
. Kod jest w C ++ 14, ale idea ma zastosowanie również w C ++ 98. Zobacz przykład na żywo .źródło
Zwarcie operatorów logicznych jest dozwolone, ponieważ jest to „optymalizacja” w ocenie powiązanych tabel prawdy. Jest to funkcja samej logiki i ta logika jest zdefiniowana.
Niestandardowe przeciążone operatory logiczne nie są zobowiązane do przestrzegania logiki tych tabel prawdy.
Dlatego cała funkcja musi być oceniana zgodnie z normą. Kompilator musi traktować go jako normalny przeciążony operator (lub funkcję) i nadal może stosować optymalizacje, tak jak w przypadku każdej innej funkcji.
Ludzie przeciążają operatory logiczne z różnych powodów. Na przykład; mogą mieć określone znaczenie w określonej dziedzinie, która nie jest „normalną” logiczną domeną, do której ludzie są przyzwyczajeni.
źródło
To zwarcie jest spowodowane tabelą prawdy „i” oraz „lub”. Skąd możesz wiedzieć, jaką operację ma zdefiniować użytkownik i skąd wiesz, że nie będziesz musiał oceniać drugiego operatora?
źródło
: (<condition>)
z deklaracją operatora określającą warunek, w którym drugi argument nie jest oceniany?Chcę tylko odpowiedzieć na tę jedną część. Przyczyną jest to, że funkcje wbudowane
&&
i||
wyrażenia nie są implementowane z funkcjami, tak jak są to przeciążone operatory.Posiadanie logiki zwarciowej wbudowanej w zrozumienie przez kompilator określonych wyrażeń jest łatwe. To jest jak każdy inny wbudowany przepływ sterowania.
Jednak przeciążanie operatorów jest implementowane za pomocą funkcji, które mają określone reguły, z których jedna polega na tym, że wszystkie wyrażenia używane jako argumenty są oceniane przed wywołaniem funkcji. Oczywiście można zdefiniować inne zasady, ale to trudniejsze zadanie.
źródło
&&
,||
i,
powinno być dozwolone? Fakt, że C ++ nie ma mechanizmu pozwalającego przeciążeniom zachowywać się jak cokolwiek innego niż wywołania funkcji, wyjaśnia, dlaczego przeciążenia tych funkcji nie mogą robić nic innego, ale nie wyjaśnia, dlaczego te operatory są przeciążalne w pierwszej kolejności. Podejrzewam, że prawdziwym powodem jest po prostu to, że bez większego zastanowienia zostali wrzuceni na listę operatorów.