Czy rzeczywiście istnieje powód, dla którego są przeciążone && i || nie zwarcie?

137

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 boolmają takie zachowanie, dlaczego miałoby być ograniczone do tego jednego typu? Czy jest za tym jakieś techniczne uzasadnienie?

iFreilicht
źródło
1
@PiotrS. To pytanie jest prawdopodobnie odpowiedzią. Myślę, że standard mógłby zdefiniować nową składnię właśnie w tym celu. Prawdopodobnie jak operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht
1
@PiotrS .: Zastanów logiki tri-state: {true, false, nil}. Ponieważ nil&& x == nilmoże to spowodować zwarcie.
MSalters
1
@MSalters: Zastanów się std::valarray<bool> a, b, c;, jak wyobrażasz sobie a || b || czwarcie?
Piotr Skotnicki
4
@PiotrS .: Twierdzę, że istnieje co najmniej jeden typ typu non-bool, dla którego zwarcie ma sens. Nie twierdzę, że skracanie ma sens dla każdego nie jest typu bool.
MSalters
3
Nikt jeszcze o tym nie wspomniał, ale jest też kwestia kompatybilności wstecznej. O ile nie poświęcono szczególnej uwagi ograniczeniu okoliczności, w których to zwarcie miałoby zastosowanie, takie zwarcie może złamać istniejący kod, który przeciąża operator&&lub operator||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.
David Hammen,

Odpowiedzi:

151

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:

A && B

mogłoby być postrzegane jako coś moralnego odpowiednika:

operator_&& ( A, ()=> B )

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:

  • czy powinniśmy ocenić prawostronny operand?
  • jeśli odpowiedź na powyższe pytanie brzmiała „tak”, to jak połączyć te dwa operandy?

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ń.

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...

(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:

C cresult = cleft && cright;

Kompilator generuje kod w tym celu, tak jak myślałeś, że napisałeś ten pseudo-C #:

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

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:

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

Poprzez zdefiniowanie wszystkich czterech operatorów - true, false, &i |- C # pozwala nie tylko mówią, cleft && crightale też nie zwarcie cleft & cright, a także if (cleft) if (cright) ..., a c ? consequence : alternativeiwhile(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/

Eric Lippert
źródło
1
@Deduplicator: Możesz być także zainteresowany przeczytaniem tego pytania i odpowiedzi: stackoverflow.com/questions/5965968/ ...
Eric Lippert
5
W tym przypadku myślę, że kompromis jest więcej niż uzasadniony. Skomplikowane rzeczy jest czymś, co tylko architekt biblioteki klasy muszą być zainteresowane, aw zamian za to powikłanie, to sprawia, że zużycie na łatwiejsze i bardziej intuicyjne bibliotece.
Cody Gray
1
@EricLippert Wierzę, że Envision powiedział, że zobaczył ten post i pomyślał, że to ty ... a potem zobaczył, że ma rację. Nie powiedział, że your postto nieistotne. His noticing your distinct writing stylenie ma znaczenia.
WernerCD,
5
Zespół firmy Microsoft nie otrzymuje wystarczających zasług za (1) dokonanie znacznego wysiłku, aby zrobić to, co właściwe w języku C # i (2) poprawienie go częściej niż nie.
codenheim
2
@Voo: Jeśli zdecydujesz się zaimplementować niejawną konwersję do bool, możesz użyć &&i ||bez implementacji operator true/falselub operator &/|w C # nie ma problemu. Problem pojawia się właśnie w sytuacji, gdy nie ma konwersji na boolmożliwe lub gdy nie jest się pożądanym.
Eric Lippert
43

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ż:

objectA && objectB

Sprowadza się to do:

template< typename T >
ClassA.operator&&( T const & objectB )

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 nazywając konwersję do bool, dobrze ...

objectA && obectB

robi to samo, czy teraz? Więc po co w ogóle przeciążać?

DevSolar
źródło
7
cóż, twoim błędem logicznym jest rozumowanie w ramach aktualnie zdefiniowanego języka o skutkach innego zdefiniowanego języka. w dawnych czasach wielu początkujących robiło to. „wirtualny konstruktor”. potrzeba było nadmiernej ilości wyjaśnień, aby wyrwać ich z takiego pudełkowego myślenia. w każdym razie przy zwarciu wbudowanych operatorów istnieją gwarancje braku oceny argumentów. taka gwarancja byłaby również dostępna dla przeciążeń zdefiniowanych przez użytkownika, gdyby zdefiniowano dla nich zwarcie.
Pozdrawiam i hth. - Alf
1
@iFreilicht: Powiedziałem w zasadzie to samo, co Deduplicator czy Piotr, tylko innymi słowami. W zredagowanej odpowiedzi wyjaśniłem nieco ten punkt. W ten sposób było znacznie wygodniej, niezbędne rozszerzenia językowe (np. Lambdy) nie istniały do ​​niedawna, a korzyść i tak byłaby znikoma. Kilka razy, gdy osobom odpowiedzialnym „spodobało się” coś, czego nie zrobili jeszcze twórcy kompilatorów, w 1998 r. Przyniosło to odwrotny skutek. (Patrz export.)
DevSolar
9
@iFreilicht: boolOperator 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ł.
DevSolar,
1
Muszę przyznać, że nie mogę o żadnym wymyślić. Jedynym powodem, dla którego istnieje zwarcie, jest to, że oszczędza czas na operacje na operacjach logicznych i możesz poznać wynik wyrażenia przed obliczeniem wszystkich argumentów. W przypadku innych operacji AND tak nie jest &i dlatego i &&nie są tym samym operatorem. Dzięki za pomoc w zrozumieniu tego.
iFreilicht
8
@iFreilicht: Celem zwarcia jest raczej to, że obliczenia po lewej stronie mogą ustalić prawdziwość warunku wstępnego po prawej stronie . if (x != NULL && x->foo)wymaga zwarcia, nie dla szybkości, ale dla bezpieczeństwa.
Eric Lippert
26

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 lazyzastosowana do argumentu funkcji sprawia, że ​​funkcja jest szablonem oczekującym funktora i sprawia, że ​​kompilator pakuje wyrażenie do funktora:

A operator&&(B b, __lazy C c) {return c;}

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);

Pod okładką wyglądałoby to tak:

template<class Func> A operator&&(B b, Func& f) {auto&& c = f(); return c;}
// With `f` restricted to no-argument functors returning a `C`.

// And the call:
operator&&(exp_b, [&]{return exp_c;});

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.

Deduplikator
źródło
6
@iFreilicht: Jakieś pytania dotyczące formularza „dlaczego funkcja X nie istnieje?” ma tę samą odpowiedź: aby zaistnieć, trzeba było przemyśleć tę funkcję, uznać ją za dobry pomysł, zaprojektować, określić, zaimplementować, przetestować, udokumentować i wysłać do użytkownika końcowego. Jeśli któraś z tych rzeczy się nie wydarzyła, nie ma żadnej funkcji. Jedna z tych rzeczy nie wydarzyła się w przypadku proponowanej funkcji; ustalenie, który z nich jest historycznym problemem badawczym; zacznij rozmawiać z ludźmi z komitetu projektowego, jeśli obchodzi cię, która z tych rzeczy nigdy nie została zrobiona.
Eric Lippert
1
@EricLippert: I, w zależności od przyczyny, powtarzaj, dopóki nie zostanie wdrożony: Być może uznano to za zbyt skomplikowane i nikt nie pomyślał o ponownej ocenie. Albo ponowna ocena zakończyła się z innych powodów do odrzucenia niż wcześniej. (btw: Dodano sedno komentarza)
Deduplicator
@Deduplicator W przypadku szablonów wyrażeń nie jest wymagane ani leniwe słowo kluczowe, ani wyrażenia lambda.
Sumant
Z perspektywy historycznej należy zauważyć, że oryginalny język Algol 68 miał wymuszenie „proceduralne” (jak również deproceduring, co oznacza niejawne wywoływanie funkcji bez parametrów, gdy kontekst wymaga raczej typu wyniku niż typu funkcji). Oznacza to, że wyrażenie typu T w pozycji wymagającej wartości typu „funkcja bez parametrów zwracająca T” (pisane jako „ proc T” w Algol 68) byłoby niejawnie przekształcone w treść funkcji zwracającą dane wyrażenie (niejawna lambda). Ta funkcja została usunięta (w przeciwieństwie do dekprocedowania) w wersji języka z 1973 roku.
Marc van Leeuwen
... W przypadku C ++ podobnym podejściem mogłoby być zadeklarowanie operatorów, takich jak &&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.
Marc van Leeuwen,
13

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ć na booli

  • W razie potrzeby zwarcie można łatwo wyrazić na inne sposoby.


Na przykład, jeśli klasa Tma skojarzone &&i ||operatory, to wyrażenie

auto x = a && b || c;

gdzie a, bi csą wyrażeniami typu T, może być wyrażony jako zwarcie

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);

a może jaśniej jako

auto x = [&]() -> T_op_result
{
    auto&& and_arg = a;
    auto&& and_result = (and_arg? and_arg && b : and_arg);
    if( and_result ) { return and_result; } else { return and_result || b; }
}();

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:

#include <iostream>
using namespace std;

void say( char const* s ) { cout << s; }

struct S
{
    using Op_result = S;

    bool value;
    auto is_true() const -> bool { say( "!! " ); return value; }

    friend
    auto operator&&( S const a, S const b )
        -> S
    { say( "&& " ); return a.value? b : a; }

    friend
    auto operator||( S const a, S const b )
        -> S
    { say( "|| " ); return a.value? a : b; }

    friend
    auto operator<<( ostream& stream, S const o )
        -> ostream&
    { return stream << o.value; }

};

template< class T >
auto is_true( T const& x ) -> bool { return !!x; }

template<>
auto is_true( S const& x ) -> bool { return x.is_true(); }

#define SHORTED_AND( a, b ) \
[&]() \
{ \
    auto&& and_arg = (a); \
    return (is_true( and_arg )? and_arg && (b) : and_arg); \
}()

#define SHORTED_OR( a, b ) \
[&]() \
{ \
    auto&& or_arg = (a); \
    return (is_true( or_arg )? or_arg : or_arg || (b)); \
}()

auto main()
    -> int
{
    cout << boolalpha;
    for( int a = 0; a <= 1; ++a )
    {
        for( int b = 0; b <= 1; ++b )
        {
            for( int c = 0; c <= 1; ++c )
            {
                S oa{!!a}, ob{!!b}, oc{!!c};
                cout << a << b << c << " -> ";
                auto x = SHORTED_OR( SHORTED_AND( oa, ob ), oc );
                cout << x << endl;
            }
        }
    }
}

Wynik:

000 -> !! !! || fałszywe
001 -> !! !! || prawdziwe
010 -> !! !! || fałszywe
011 -> !! !! || prawdziwe
100 -> !! && !! || fałszywe
101 -> !! && !! || prawdziwe
110 -> !! && !! prawdziwe
111 -> !! && !! prawdziwe

Tutaj każdy !!bang-bang pokazuje konwersję na bool, 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.

Pozdrawiam i hth. - Alf
źródło
Podoba mi się twoje zamiany zwarć, zwłaszcza trójskładnikowe, które są tak bliskie, jak to tylko możliwe.
iFreilicht
Brakuje zwarcia &&- musiałby być dodatkowy wiersz podobny do if (!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.
Arne Mertz
@ArneMertz: Twój komentarz dotyczący „zaginięcia” jest najwyraźniej bez znaczenia. komentarz o tym, o co chodzi, tak, jestem tego świadomy. konwersja do booljest konieczna do zrobienia zwarcia.
Pozdrawiam i hth. - Alf
@ Cheersandhth.-Alf komentarz o zaginięciu dotyczył pierwszej wersji Twojej odpowiedzi, w której dokonałeś 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.
Arne Mertz
@ArneMertz: OK, zmiana wersji, przepraszam za wolne edytowanie. Ponownie ograniczone, nie, to wynik operatora musi być ograniczony, ponieważ musi zostać przekonwertowany na bool, aby sprawdzić, czy nie występują krótkie krążenia dalszych operatorów w wyrażeniu. Na przykład wynik a && bmusi zostać przekonwertowany na, boolaby sprawdzić, czy nie występuje krótkie krążenie logicznego LUB w a && b || c.
Pozdrawiam i hth. - Alf
5

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że x = y && zdowolnie je zwierać, podczas gdy wyrażenie ma wartość nonboolean yi zmusi prowadzić do wywołania funkcji takiej jak X operator&& (Y, Z). Oznaczałoby to, że y && zjest 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 przypadku newoperatora, który jest tłumaczony na wywołanie funkcji, operator newpo 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 Yjest to możliwe X, 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:

X operator&&(Y const& y, Z const& z)
{
  if (shortcircuitCondition(y))
    return shortcircuitEvaluation(y);

  <"Syntax for an evaluation-Point for z here">

  return actualImplementation(y,z);
}

Rzadko chce się przeciążać, operator||a operator&&ponieważ rzadko zdarza się, że pisanie a && bjest 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&&i operator||- po prostu aby dojść do wniosku, że nie jest to mniejszy wysiłek niż napisanie logiki odręcznie.

Arne Mertz
źródło
Czy koszt naprawdę jest tak wysoki? Język programowania D pozwala zadeklarować parametry, lazyktó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 ()); }}
BlackJack
@BlackJack ten leniwy parametr można zaimplementować, akceptując a std::function<B()>, co spowodowałoby pewien narzut. Lub jeśli chcesz to wbudować, zrób to template <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ć. lazySkładnia może być bardziej wygodne, ale ma pewien kompromis wydajności.
Arne Mertz
1
Jednym z problemów związanych z std::functionwersją versus lazyjest to, że pierwszy można ocenić wielokrotnie. Leniwy parametr, fooktóry jest używany jako foo+foonadal oceniany tylko raz.
MSalters
„użycie zwarć byłoby ograniczone do przypadków, w których Y jest konwetowalne z X”… nie, jest ograniczone do przypadków, w których Xmożna je obliczyć Ysamodzielnie. 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”.
Mooing Duck
1
@Sumant, że mogą. Ale możesz również operator&&ręcznie napisać logikę zwarcia niestandardowego . Pytanie nie brzmi, czy to możliwe, ale dlaczego nie ma krótkiej wygodnej drogi.
Arne Mertz
5

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 lazyi 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 instancji Exprszablonu. Drzewo jest oceniane oddzielnie po zakończeniu budowy.

Poniższy kod implementuje skróty &&i ||operatory dla klasy, So ile zapewnia logical_andi logical_orwolne funkcje i można go przekonwertować na bool. Kod jest w C ++ 14, ale idea ma zastosowanie również w C ++ 98. Zobacz przykład na żywo .

#include <iostream>

struct S
{
  bool val;

  explicit S(int i) : val(i) {}  
  explicit S(bool b) : val(b) {}

  template <class Expr>
  S (const Expr & expr)
   : val(evaluate(expr).val)
  { }

  template <class Expr>
  S & operator = (const Expr & expr)
  {
    val = evaluate(expr).val;
    return *this;
  }

  explicit operator bool () const 
  {
    return val;
  }
};

S logical_and (const S & lhs, const S & rhs)
{
    std::cout << "&& ";
    return S{lhs.val && rhs.val};
}

S logical_or (const S & lhs, const S & rhs)
{
    std::cout << "|| ";
    return S{lhs.val || rhs.val};
}


const S & evaluate(const S &s) 
{
  return s;
}

template <class Expr>
S evaluate(const Expr & expr) 
{
  return expr.eval();
}

struct And 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? logical_and(temp, evaluate(r)) : temp;
  }
};

struct Or 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? temp : logical_or(temp, evaluate(r));
  }
};


template <class Op, class LExpr, class RExpr>
struct Expr
{
  Op op;
  const LExpr &lhs;
  const RExpr &rhs;

  Expr(const LExpr& l, const RExpr & r)
   : lhs(l),
     rhs(r)
  {}

  S eval() const 
  {
    return op(lhs, rhs);
  }
};

template <class LExpr>
auto operator && (const LExpr & lhs, const S & rhs)
{
  return Expr<And, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator && (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<And, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

template <class LExpr>
auto operator || (const LExpr & lhs, const S & rhs)
{
  return Expr<Or, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator || (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<Or, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

std::ostream & operator << (std::ostream & o, const S & s)
{
  o << s.val;
  return o;
}

S and_result(S s1, S s2, S s3)
{
  return s1 && s2 && s3;
}

S or_result(S s1, S s2, S s3)
{
  return s1 || s2 || s3;
}

int main(void) 
{
  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << and_result(S{i}, S{j}, S{k}) << std::endl;

  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << or_result(S{i}, S{j}, S{k}) << std::endl;

  return 0;
}
Sumant
źródło
5

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.

Czy rzeczywiście jest powód, dla którego jest przeciążony &&i ||nie powoduje zwarcia?

Niestandardowe przeciążone operatory logiczne niezobowiązane do przestrzegania logiki tych tabel prawdy.

Ale dlaczego tracą to zachowanie, gdy są przeciążone?

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.

Niall
źródło
4

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?

nj-ath
źródło
Jak wspomniano w komentarzach i w odpowiedzi @Deduplicators, byłoby to możliwe dzięki dodatkowej funkcji językowej. Wiem, że teraz to nie działa. Moje pytanie dotyczyło przyczyny braku takiej funkcji.
iFreilicht
Cóż, z pewnością byłaby to skomplikowana funkcja, biorąc pod uwagę, że musimy zaryzykować definicję jej użytkownika!
nj-ath,
A co : (<condition>)z deklaracją operatora określającą warunek, w którym drugi argument nie jest oceniany?
iFreilicht
@iFreilicht: Nadal potrzebujesz alternatywnego ciała funkcji jednoargumentowej.
MSalters
3

ale operatory dla bool mają takie zachowanie, dlaczego miałoby być ograniczone do tego jednego typu?

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.

bames53
źródło
1
Zastanawiam się, czy każdy rozważono kwestię , czy przeciążeń &&, ||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.
supercat