Inny operator rzutowania wywoływany przez różne kompilatory

80

Rozważmy następujący krótki program w C ++:

#include <iostream>

class B {
public:
    operator bool() const {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

Jeśli skompiluję go na różnych kompilatorach, otrzymam różne wyniki. Z Clang 3.4 i GCC 4.4.7 drukuje true, podczas gdy Visual Studio 2013 drukuje false, co oznacza, że ​​wywołują różne operatory rzutowania pod adresem (bool)b. Jakie jest prawidłowe zachowanie zgodnie z normą?

W moim rozumieniu operator bool()nie potrzebuje nawrócenia, podczas gdy operator int()wymagałoby intdo boolkonwersji, więc kompilator powinien wybrać pierwszy. Czy constcoś z tym robi, czy konwersja const jest uważana przez kompilator za bardziej „kosztowną”?

Jeśli usunę plik const, wszystkie kompilatory w równym stopniu produkują falsejako wyjście. Z drugiej strony, jeśli połączę te dwie klasy razem (oba operatory będą w tej samej klasie), wszystkie trzy kompilatory wygenerują truewynik.

buc
źródło
3
Jak rozumiem, B2 ma 2 operatory: B :: operator bool () i B2 :: operator int (). Oba operatory nie są operatorami const i istnieje różnica między wersjami const i non-const. Możesz dodać wersję const bool do B i wersję const int do B2 i zobaczyć zmiany.
Tanuki,
1
constjest tutaj kluczem. Oba operatory są jednakowo stałe lub nie, zachowują się zgodnie z oczekiwaniami, boolwersja "wygrywa". Z różną stałością, zawsze wersja bez stałej „wygrywa” na każdej platformie. Z klasami pochodnymi i różnymi stałościami operatorów, VS wydaje się mieć błąd, ponieważ zachowuje się inaczej niż dwa pozostałe kompilatory.
buc
czy różne domyślne konstruktory mogą być odpowiedzialne za różne zachowanie?
ldgorman
16
EDG również drukuje true. Jeśli GCC, Clang i EDG zgadzają się, a MSVC nie zgadza się, zwykle oznacza to, że MSVC się myli.
Jonathan Wakely,
1
Uwaga: ogólnie w C ++ zwracany typ nie uczestniczy w rozpoznawaniu przeciążenia.
Matthieu M.,

Odpowiedzi:

51

Norma stwierdza:

Funkcja konwersji w klasie pochodnej nie ukrywa funkcji konwersji w klasie bazowej, chyba że dwie funkcje są konwertowane na ten sam typ.

§12.3 [class.conv]

Co oznacza, że operator boolnie jest to ukryte przez operator int.

Norma stwierdza:

Podczas rozwiązywania przeciążenia domniemany argument obiektu jest nie do odróżnienia od innych argumentów.

§13.3.3.1 [over.match.funcs]

„Domniemany argument obiektu” w tym przypadku to b, który jest typu B2 &. operator boolwymaga const B2 &, więc kompilator będzie musiał dodać const to bdo wywołania operator bool. To - wszystkie inne rzeczy są równe - operator intlepiej pasuje.

Standard stwierdza, że ​​a static_cast(który wykonuje rzutowanie w stylu C w tym przypadku) może przekonwertować na typ T(w tym przypadku int), jeśli:

deklaracja T t(e);jest dobrze sformułowana, dla jakiejś wymyślonej zmiennej tymczasowej t.

§5.2.9 [expr.static.cast]

Dlatego intmożna przekonwertować na a bool, a boolmożna również przekształcić w a bool.

Norma stwierdza:

Uwzględnione są funkcje konwersji Si jej klasy bazowe. Te niejawne funkcje konwersji, które nie są ukryte w ramach Si dają typ T lub typ, który można przekonwertować na typ Tza pomocą standardowej sekwencji konwersji, są funkcjami kandydującymi.

§13.3.1.5 [over.match.conv]

Więc zestaw przeciążenia składa się z operator inti operator bool. Wszystkie inne rzeczy są równe, operator intto lepsze dopasowanie (ponieważ nie musisz dodawać stałości). Dlatego operator intnależy wybrać.

Zauważ, że (być może wbrew intuicji) standard nie bierze pod uwagę zwracanego typu (tj. Typu, na który te operatory konwertują) po ich dodaniu do zbioru przeciążeń (jak ustalono powyżej), pod warunkiem, że sekwencja konwersji dla argumentów jednego z są one lepsze od kolejności konwersji dla argumentów drugiego (co ze względu na stałość ma miejsce w tym przypadku).

Norma stwierdza:

Biorąc pod uwagę te definicje, wykonalna funkcja F1 jest zdefiniowana jako lepsza funkcja niż inna żywotna funkcja F2, jeśli dla wszystkich argumentów i, ICSi (F1) nie jest gorszą sekwencją konwersji niż ICSi (F2), a następnie

  • dla jakiegoś argumentu j ICSj (F1) jest lepszą sekwencją konwersji niż ICSj (F2), a jeśli nie to,
  • kontekstem jest inicjalizacja przez konwersję zdefiniowaną przez użytkownika, a standardowa sekwencja konwersji od typu zwracanego F1 do typu docelowego (tj. typ inicjowanej jednostki) jest lepszą sekwencją konwersji niż standardowa sekwencja konwersji z typu zwracanego F2 do typu docelowego.

§13.3.3 [over.match.best]

W tym przypadku jest tylko jeden argument (niejawny thisparametr). Sekwencja konwersji dla B2 &=> B2 &(do wywołania operator int) jest lepsza niż B2 &=> const B2 &(do wywołania operator bool) i dlatego operator intjest wybierana z zestawu przeciążenia bez względu na fakt, że w rzeczywistości nie jest ona konwertowana bezpośrednio na bool.

Robert Allan Hennigan Leahy
źródło
2
Możesz pobrać N3337, który był (moim zdaniem) pierwszym szkicem po C ++ 11 i zawiera tylko bardzo, bardzo drobne zmiany tutaj za darmo. Jeśli chodzi o znalezienie odpowiedniej części standardu, zwykle jest to kwestia oceny sekcji pliku PDF, dowiedzenia się, gdzie są rzeczy (poprzez sprawdzenie / cytując standard) i wyszukanie odpowiednich słów kluczowych za pomocą CTRL + F.
Robert Allan Hennigan Leahy
1
@ikh Gdzie znajdę aktualne dokumenty w standardzie C lub C ++? jest najlepszym pytaniem do tego i, o ile wiem, obejmuje wszystkie wersje robocze, które są dostępne zarówno dla C, jak i C ++.
Shafik Yaghmour,
2
To, co powiedziałem, „jest stosowane dopiero po ustaleniu, która sekwencja konwersji jest lepsza”. Myślę jednak, że jest to ważna część odpowiedzi: istnieje (w teorii) konflikt między konwersją domniemanego argumentu obiektu a „konwersją typu zwracanego” i jest on jednoznacznie rozwiązywany na różnych etapach rozwiązywania problemu z przeciążeniem. [over.match.best] /1.4 wyraźnie wskazuje na oddzielenie tych kroków i dlaczego ostateczna standardowa sekwencja konwersji nie ma znaczenia w tym przypadku.
dyp
2
Zredagowałem odpowiedź, aby uwzględnić twoją sugestię, wyjaśniając, dlaczego typ zwrotu tych operatorów konwersji nie jest brany pod uwagę przy wyborze „najlepszego”.
Robert Allan Hennigan Leahy
2
@RobertAllanHenniganLeahy Skoro o to chodziło w pytaniu, czy nie powinno ono pojawić się gdzieś w odpowiedzi, a nie tylko w komentarzu?
Barmar,
9

Krótki

Funkcja konwersji operator int()jest wybierana przez clang over, operator bool() constponieważ bnie jest kwalifikowana jako const, podczas gdy operator konwersji dla bool to.

Krótki rozumowanie jest to, że funkcje kandydujących dla rozdzielczości przeciążenia (z utajonego parametru obiektu w miejscu), do konwersji bdo bool

operator bool (B2 const &);
operator int (B2 &);

gdzie drugi jest lepiej dopasowany, ponieważ bnie jest określony jako const.

Jeśli obie funkcje mają tę samą kwalifikację (obie constlub nie), operator booljest wybierana, ponieważ zapewnia bezpośrednią konwersję.

Konwersja za pomocą notacji rzutowej, analizowana krok po kroku

Jeśli zgodzimy się, że boolean ostream inserter (std :: basic_ostream :: operator << (bool val) zgodnie z [ostream.inserters.arithmetic]) jest wywoływany z wartością wynikającą z konwersji bna boolto możemy wykopać się do tej konwersji .

1. Wyrażenie obsady

Obsada b do bool

(bool)b

ocenia do

static_cast<bool>(b)

zgodnie z C ++ 11, 5.4 / 4 [wyraż.cast], ponieważ const_castnie ma zastosowania (nie można tu dodawać ani usuwać stałej).

Ta statyczna konwersja jest dozwolona według C ++ 11, 5.2.9 / 4 [expr.static.cast] , jeśli bool t(b);dla wymyślonej zmiennej t jest dobrze sformułowana. Takie instrukcje nazywane są bezpośrednią inicjalizacją zgodnie z C ++ 11, 8.5 / 15 [dcl.init] .

2. Inicjalizacja bezpośrednia bool t(b);

Klauzula 16 najmniej wymienionego standardowego akapitu stwierdza (moje wyróżnienie):

Semantyka inicjatorów jest następująca. Typ docelowy to typ inicjowanego obiektu lub odwołania, a typ źródłowy to typ wyrażenia inicjatora.

[…]

[...] jeśli typ źródła jest (prawdopodobnie kwalifikowany przez cv) typem klasy, uwzględniane są funkcje konwersji .

Odpowiednie funkcje konwersji są wyliczane, a najlepsza jest wybierana poprzez rozpoznawanie przeciążenia.

2.1 Jakie funkcje konwersji są dostępne?

Dostępne funkcje konwersji to, operator int ()a operator bool() constponieważ C ++ 11, 12.3 / 5 [class.conv] mówi nam:

Funkcja konwersji w klasie pochodnej nie ukrywa funkcji konwersji w klasie bazowej, chyba że dwie funkcje są konwertowane na ten sam typ.

Podczas gdy C ++ 11, 13.3.1.5/1 [over.match.conv] stwierdza:

Uwzględniono funkcje konwersji S i jego klas bazowych.

gdzie S jest klasą, z której zostanie przekonwertowana.

2.2 Które funkcje konwersji mają zastosowanie?

C ++ 11, 13.3.1.5/1 [over.match.conv] (moje podkreślenie):

1 [...] Zakładając, że „cv1 T” jest typem inicjowanego obiektu, a „cv S” jest typem wyrażenia inicjatora, przy czym S jest typem klasy, funkcje kandydatów są wybierane w następujący sposób: Konwersja rozważane są funkcje S i jego klas bazowych. Te niejawne funkcje konwersji, które nie są ukryte w S i dają typ T lub typ, który można przekonwertować na typ T za pomocą standardowej sekwencji konwersji, są funkcjami kandydującymi.

Dlatego operator bool () constma zastosowanie, ponieważ nie jest ukryty w B2środku i daje bool.

Część z naciskiem w ostatnim cytacie standardowym ma znaczenie dla konwersji przy użyciu, operator int ()ponieważ intjest typem, który można przekonwertować na bool za pomocą standardowej sekwencji konwersji. Konwersja z intna boolnie jest nawet sekwencją, ale zwykłą konwersją bezpośrednią, która jest dozwolona w C ++ 11, 4.12 / 1 [conv.bool]

Wartość pr arytmetycznego, wyliczenia bez zakresu, wskaźnika lub wskaźnika do typu elementu członkowskiego można przekonwertować na prvalue typu bool. Wartość zerowa, pusta wartość wskaźnika lub pusta wartość wskaźnika elementu członkowskiego jest konwertowana na fałsz; każda inna wartość jest konwertowana na prawdę.

Oznacza to, że operator int ()ma to również zastosowanie.

2.3 Która funkcja konwersji jest wybrana?

Wybór odpowiedniej funkcji konwersji odbywa się poprzez rozwiązanie przeciążenia ( C ++ 11, 13.3.1.5/1 [over.match.conv] ):

Rozdzielczość przeciążenia służy do wybierania funkcji konwersji do wywołania.

Istnieje jeden specjalny „dziwactwo”, jeśli chodzi o rozwiązywanie przeciążeń funkcji składowych klasy: niejawny parametr obiektu ”.

Per C ++ 11, 13.3.1 [over.match.funcs] ,

[...] zarówno statyczne, jak i niestatyczne funkcje składowe mają niejawny parametr obiektu [...]

gdzie typ tego parametru dla niestatycznych funkcji składowych - zgodnie z klauzulą ​​4 - to:

  • „L-wartość odniesienia do cv X” dla funkcji zadeklarowanych bez kwalifikatora ref lub z kwalifikatorem & ref

  • „Rvalue reference to cv X” dla funkcji zadeklarowanych z kwalifikatorem && ref

gdzie X to klasa, której składową jest funkcja, a cv to kwalifikacja cv na deklaracji funkcji składowej.

Oznacza to, że (na C ++ 11, 13.3.1.5/2 [over.match.conv] ), podczas inicjalizacji przez funkcję konwersji,

Lista argumentów ma jeden argument, którym jest wyrażenie inicjalizujące. [Uwaga: ten argument zostanie porównany z niejawnym parametrem obiektu funkcji konwersji. —End note]

Kandydujące funkcje do rozwiązywania problemów z przeciążeniem to:

operator bool (B2 const &);
operator int (B2 &);

Oczywiście operator int ()jest to lepsze dopasowanie, jeśli zażądano konwersji przy użyciu niestałego obiektu typu, B2ponieważ operator bool ()wymagana jest konwersja kwalifikacji.

Jeśli obie funkcje konwersji mają tę samą kwalifikację const, rozwiązanie przeciążenia tych funkcji już nie wystarczy. W takim przypadku ma miejsce ranking konwersji (kolejności).

3. Dlaczego jest operator bool ()wybierany, skoro obie funkcje konwersji mają tę samą kwalifikację const?

Konwersja z B2do booljest sekwencją konwersji zdefiniowaną przez użytkownika ( C ++ 11, 13.3.3.1.2 / 1 [over.ics.user] )

Sekwencja konwersji zdefiniowana przez użytkownika składa się z początkowej sekwencji konwersji standardowej, po której następuje konwersja zdefiniowana przez użytkownika, po której następuje druga standardowa sekwencja konwersji.

[...] Jeśli konwersja zdefiniowana przez użytkownika jest określona przez funkcję konwersji, początkowa standardowa sekwencja konwersji przekształca typ źródłowy na niejawny parametr obiektu funkcji konwersji.

C ++ 11, 13.3.3.2/3 [over.ics.rank]

[...] definiuje częściowe uporządkowanie niejawnych sekwencji konwersji na podstawie relacji lepsza sekwencja konwersji i lepsza konwersja.

[...] Zdefiniowana przez użytkownika sekwencja konwersji U1 jest lepszą sekwencją konwersji niż inna zdefiniowana przez użytkownika sekwencja konwersji U2, jeśli zawiera tę samą zdefiniowaną przez użytkownika funkcję konwersji lub konstruktora lub inicjalizację agregatu, a druga standardowa sekwencja konwersji U1 jest lepsza niż druga standardowa sekwencja konwersji U2.

Drugi średnia konwersji przypadku operator bool()jest booldo bool(konwersja tożsamości), podczas gdy drugi standard konwersji w przypadku operator int ()jest intdo boolktórych to logiczna konwersji.

Dlatego sekwencja konwersji przy użyciu operator bool ()jest lepsza, jeśli obie funkcje konwersji mają tę samą kwalifikację const.

Pixelchemist
źródło
W większości przypadków jest całkiem intuicyjne, że to, co robisz z wynikiem, nie wpływa na sposób jego obliczania. Sposób obliczania a/bjest taki sam, niezależnie od tego, czy kod jest float f = a/b;czy int f = a/b;. Ale to przypadek, w którym może to być nieco zaskakujące.
David Schwartz,
1

Typ bool w C ++ ma dwie wartości - prawda i fałsz z odpowiadającymi im wartościami 1 i 0. Nieodłączne zamieszanie można uniknąć, jeśli dodasz operator bool w klasie B2, który jawnie wywołuje operator bool klasy bazowej (B), a następnie pojawi się wynik jako fałszywe. Oto mój zmodyfikowany program. Wtedy operator bool oznacza operator bool, a nie operator int w jakikolwiek sposób.

#include <iostream>

class B {
public:
    operator bool() const {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
    operator bool() {
        return B::operator bool();
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

W twoim przykładzie (bool) b próbował wywołać operator bool dla B2, B2 odziedziczył operator bool, a operator int, na podstawie reguły dominacji, zostaje wywołany operator int, a dziedziczony operator bool w B2. Jednak dzięki jawnemu umieszczeniu operatora bool w samej klasie B2 problem zostanie rozwiązany.

Dr. Debasish Jana
źródło
8
To fajne rozwiązanie, aby napisać program, ale nie wyjaśnia, dlaczego ta dziwna rzecz występuje szczegółowo.
ikh
4
true and false with corresponding values 1 and 0To nadmierne uproszczenie. truekonwertuje na liczbę całkowitą 1, ale to nie znaczy, że „ma” wartość 1. Rzeczywiście, truemoże być 42pod spodem.
Wyścigi lekkości na orbicie,
Posiadanie jawnego operatora bool w B2 pozwala uniknąć nieporozumień i zależności kompilatora od tego, co jest wywoływane w B2, operator int lub operator bool.
Dr. Debasish Jana
Nie ma zależności od kompilatora. Standard wymaga, aby 0 konwertowało na fałsz, a 1 na prawdę.
Puppy
1
@LightnessRacesinOrbit Bardziej do rzeczy, być może: a boolnigdy nie ma wartości 0 ani 1; jedyne wartości, jakie może przyjąć, to falsei true(które są konwertowane na 0 i 1, jeśli booljest konwertowany na typ całkowity).
James Kanze,
0

Niektóre z poprzednich odpowiedzi zawierają już wiele informacji.

Mój wkład jest taki, że „operacje rzutowania” są kompilowane podobnie do „operacji przeciążonych”, proponuję utworzyć funkcję z unikalnym identyfikatorem dla każdej operacji, a później zastąpić ją wymaganym operatorem lub rzutowaniem.

#include <iostream>

class B {
public:
    bool ToBool() const {
        return false;
    }
};

class B2 : public B {
public:
    int ToInt() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << b.ToBool() << std::endl;
}

A później zastosuj operator lub rzut.

#include <iostream>

class B {
public:
    operator bool() {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

Tylko moje 2 centy.

umlcat
źródło