Dlaczego odwołania nie są „stałymi” w C ++?


86

Wiemy, że „zmienna stała” oznacza, że ​​raz przypisana nie można zmienić zmiennej, na przykład:

int const i = 1;
i = 2;

Powyższy program nie skompiluje się; gcc wyświetla komunikat z błędem:

assignment of read-only variable 'i'

Nie ma problemu, rozumiem to, ale następujący przykład jest poza moim zrozumieniem:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

Wyprowadza

true
false

Dziwne. Wiemy, że gdy odniesienie jest powiązane z nazwą / zmienną, nie możemy zmienić tego powiązania, zmieniamy związany z nim obiekt. Więc przypuszczam, że typ ripowinien być taki sam, jak i: kiedy ijest int const, dlaczego rinie const?


3
Jest też boolalpha(cout)bardzo nietypowy. Możesz std::cout << boolalphazamiast tego zrobić .
isanae

6
To tyle, jeśli chodzi o ribycie „aliasem”, którego nie da się odróżnić i.
Kaz

1
Dzieje się tak, ponieważ odniesienie jest zawsze ograniczone tym samym obiektem. ijest również odniesieniem, ale ze względów historycznych nie deklarujesz go jako takiego w wyraźny sposób. Tak więc ijest to odniesienie, które odnosi się do magazynu i rijest odniesieniem, które odnosi się do tego samego magazynu. Ale nie ma różnicy w naturze pomiędzy i a ri. Ponieważ nie można zmienić powiązania odniesienia, nie ma potrzeby kwalifikowania go jako const. I pozwól mi powiedzieć, że komentarz @Kaz jest znacznie lepszy niż zweryfikowana odpowiedź (nigdy nie wyjaśniaj odwołań za pomocą wskaźników, ref to nazwa, a ptr to zmienna).
Jean-Baptiste Yunès

1
Fantastyczne pytanie. Przed obejrzeniem tego przykładu spodziewałbym is_constsię powrotu również truew tym przypadku. Moim zdaniem jest to dobry przykład tego, dlaczego constjest zasadniczo zacofany; „zmienny” atrybut (a la Rusta mut) wydaje się, że byłby bardziej spójny.
Kyle Strand

1
Być może tytuł powinien zostać zmieniony na „dlaczego jest is_const<int const &>::valuefałszywy?” lub podobne; Staram się dostrzec jakiekolwiek znaczenie tego pytania poza pytaniem o zachowanie cech typu
MM

Odpowiedzi:


52

Może się to wydawać sprzeczne z intuicją, ale myślę, że sposobem na zrozumienie tego jest uświadomienie sobie, że pod pewnymi względami odniesienia są traktowane syntaktycznie jak wskaźniki .

Wydaje się to logiczne dla wskaźnika :

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Wynik:

true
false

Jest to logiczne, ponieważ wiemy, że to nie obiekt wskaźnika jest const (można go wskazać gdzie indziej), ale obiekt, na który jest wskazywany.

Tak więc poprawnie widzimy, że stałość samego wskaźnika została zwrócona jako false.

Jeśli chcemy zrobić sam wskaźnik , constmusimy powiedzieć:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* const ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Wynik:

true
true

Myślę więc, że widzimy analogię syntaktyczną z odniesieniem .

Jednak odniesienia są semantycznie różni się od wskaźników, zwłaszcza w jednej kluczowej szacunku, nie wolno ponownie powiązać odniesienie do innego obiektu po związaniu.

Więc choć referencje mają taką samą składnię jak wskaźniki zasady są różne, a więc nie pozwala nam język od deklarowania odwołanie sam constjak ten:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const& const ri = i; // COMPILE TIME ERROR!
    cout << is_const<decltype(ri)>::value << endl;
}

Zakładam, że nie możemy tego zrobić, ponieważ wydaje się, że nie jest to potrzebne, gdy reguły języka zapobiegają odbiciu odniesienia w taki sam sposób, jak mógłby to zrobić wskaźnik (jeśli nie jest zadeklarowany const).

A więc odpowiadając na pytanie:

P) Dlaczego „odwołanie” nie jest „stałą” w C ++?

W twoim przykładzie składnia sprawia, że ​​odniesienie do rzeczy jest consttakie samo, jak w przypadku deklarowania wskaźnika .

Słusznie lub niesłusznie nie wolno nam podawać samego odniesienia , constale gdybyśmy tak było, wyglądałoby to tak:

int const& const ri = i; // not allowed

Q) wiemy, że gdy odniesienie zostanie powiązane z nazwą / zmienną, nie możemy zmienić tego powiązania, zmieniamy powiązany z nim obiekt. Więc przypuszczam, że typ ripowinien być taki sam, jak i: kiedy ijest a int const, dlaczego rinie const?

Dlaczego decltype()nie przenosi się tego na obiekt, do którego odwołuje się referencja ?

Przypuszczam, że dotyczy to semantycznej równoważności ze wskaźnikami, a być może także funkcją decltype()(zadeklarowanego typu) jest spojrzenie wstecz na to, co zostało zadeklarowane przed wiązaniem.


13
Wygląda na to, że mówisz „odniesienia nie mogą być stałe, ponieważ zawsze są stałe”?
ruakh

2
Może być prawdą, że „ semantycznie wszystkie działania na nich po ich związaniu są przenoszone do obiektu, do którego się odnoszą”, ale PO spodziewał się, że to również będzie miało zastosowanie decltype, i stwierdził, że tak się nie stało.
ruakh

10
Jest tu wiele meandrujących przypuszczeń i wątpliwie mających zastosowanie analogii do wskaźników, co wydaje mi się, że myli sprawę bardziej niż to konieczne, gdy sprawdzanie standardu, tak jak zrobił to sonyuanyao, prowadzi prosto do prawdziwego punktu: odniesienie nie jest typem kwalifikowalnym cv, dlatego nie może być const, dlatego std::is_constmusi wrócić false. Mogli zamiast tego użyć sformułowania, które oznaczało, że musi wrócić true, ale tak się nie stało . Otóż ​​to! Wszystkie te rzeczy o wskaźnikach, „zakładam”, „przypuszczam” itp. Nie dają żadnego prawdziwego wyjaśnienia.
underscore_d

1
@underscore_d To prawdopodobnie lepsze pytanie do czatu niż do sekcji komentarzy tutaj, ale czy masz przykład „niepotrzebnego zamieszania” z tego sposobu myślenia o odniesieniach? Jeśli można je wdrożyć w ten sposób, jaki jest problem z myśleniem o nich w ten sposób? (Nie sądzę, żeby to pytanie się liczyło, ponieważ decltypenie jest operacją w czasie wykonywania, więc idea „w czasie wykonywania, odwołania zachowują się jak wskaźniki”, czy jest poprawna czy nie, tak naprawdę nie ma zastosowania.
Kyle Strand

1
Odwołania nie są również traktowane składniowo jak wskaźniki. Mają inną składnię do zadeklarowania i użycia. Więc nie jestem nawet pewien, co masz na myśli.
Jordan Melo

55

dlaczego „ri” nie jest „const”?

std::is_const sprawdza, czy typ ma wartość stałą, czy nie.

Jeśli T jest typem kwalifikowanym jako const (to jest const lub const volatile), zapewnia stałą składową równą true. W przypadku każdego innego typu wartość to false.

Ale odwołanie nie może być kwalifikowane jako const. Referencje [dcl.ref] / 1

Odwołania kwalifikowane jako Cv są źle sformułowane, z wyjątkiem sytuacji, gdy kwalifikatory cv są wprowadzane przy użyciu nazwy typu typedef ([dcl.typedef], [temp.param]) lub specyfikatora decltype ([dcl.type.simple]) , w takim przypadku kwalifikatory cv są ignorowane.

Więc is_const<decltype(ri)>::valuezwróci, falseponieważ ri(odwołanie) nie jest typem kwalifikowanym przez stałą. Jak powiedziałeś, nie możemy ponownie powiązać referencji po inicjalizacji, co implikuje, że referencja jest zawsze „stała”, z drugiej strony odwołanie kwalifikowane w postaci stałej lub odwołanie niekwalifikowane w postaci stałej mogą w rzeczywistości nie mieć sensu.


5
Prosto do standardu, a zatem prosto do sedna, a nie do wszystkich głupawych przypuszczeń przyjętej odpowiedzi.
underscore_d

5
@underscore_d To dobra odpowiedź, dopóki nie zauważysz, że operator również pytał dlaczego. „Ponieważ tak mówi” nie jest zbyt przydatne w tym aspekcie pytania.
simpleuser

5
@ user9999999 Druga odpowiedź również nie odpowiada na ten aspekt. Jeśli odniesienia nie można odbić, dlaczego nie is_constwraca true? Ta odpowiedź próbuje narysować analogię do tego, jak wskaźniki mogą być opcjonalnie zmieniane, podczas gdy odniesienia nie są - i robiąc to, prowadzi do sprzeczności z tym samym powodem. Nie jestem pewien, czy istnieje prawdziwe wyjaśnienie tak czy inaczej, inne niż nieco arbitralna decyzja tych, którzy piszą Standard, i czasami jest to najlepsze, na co możemy mieć nadzieję. Stąd ta odpowiedź.
underscore_d

2
Myślę, że innym ważnym aspektem jest to, że niedecltype jest to funkcja i dlatego działa bezpośrednio na samym odnośniku, a nie na obiekcie, do którego się odnosi. (Być może jest to bardziej istotne w przypadku odpowiedzi „odniesienia są w zasadzie wskazówkami”, ale nadal uważam, że jest to część tego, co sprawia, że ​​ten przykład jest zagmatwany i dlatego warto o nim tutaj wspomnieć.)
Kyle Strand

3
Aby dokładniej wyjaśnić komentarz @KyleStrand: decltype(name)działa inaczej niż generał decltype(expr). Na przykład decltype(i)to zadeklarowany typ ito const int, podczas gdy decltype((i))byłby int const &.
Daniel Schepler


8

Dlaczego nie ma makr const? Funkcje? Literały? Nazwy typów?

const rzeczy są tylko podzbiorem niezmiennych rzeczy.

Ponieważ typy referencyjne to tylko te - typy - może mieć sens wymaganie const na nich wszystkich -kwalifikatorów w celu uzyskania symetrii z innymi typami (szczególnie z typami wskaźnikowymi), ale byłoby to bardzo uciążliwe i bardzo szybko.

Gdyby C ++ miał domyślnie niezmienne obiekty, wymagając mutablesłowa kluczowego na czymkolwiek nie chciałbyś być const, to byłoby to łatwe: po prostu nie pozwól programistom dodawaćmutable do typów referencyjnych.

W obecnej sytuacji są one niezmienne bez zastrzeżeń.

A ponieważ nie są one const-kwalifikowane, prawdopodobnie byłoby bardziej zagmatwane , gdyby is_consttyp referencyjny był prawdziwy.

Uważam, że jest to rozsądny kompromis, zwłaszcza że niezmienność jest i tak wymuszona przez sam fakt, że nie istnieje składnia, która mogłaby zmienić odniesienie.


6

To jest dziwactwo / funkcja w C ++. Chociaż nie myślimy o referencjach jako o typach, w rzeczywistości „siedzą” one w systemie typów. Chociaż wydaje się to niezręczne (biorąc pod uwagę, że gdy używane są odwołania, semantyka odwołań występuje automatycznie, a odniesienie „znika z drogi”), istnieją pewne uzasadnione powody, dla których odniesienia są modelowane w systemie typów zamiast jako oddzielny atrybut poza rodzaj.

Po pierwsze, rozważmy, że nie każdy atrybut zadeklarowanej nazwy musi znajdować się w systemie typów. Z języka C mamy „klasę pamięci” i „powiązanie”. Nazwę można wprowadzić jako extern const int ri, gdzie externoznacza statyczną klasę pamięci i obecność powiązania. Ten typ jest sprawiedliwy const int.

C ++ oczywiście obejmuje pogląd, że wyrażenia mają atrybuty, które są poza systemem typów. Język ma teraz koncepcję „klasy wartości”, która jest próbą uporządkowania rosnącej liczby atrybutów innych niż typowe, które może wykazywać wyrażenie.

Jednak odniesienia są typami. Czemu?

Kiedyś wyjaśniono w samouczkach C ++, że deklaracja taka jak const int &riwprowadzona rijako posiadająca typ const int, ale semantyka odwołań. Ta semantyka odniesienia nie była typem; był to po prostu rodzaj atrybutu wskazującego na niezwykły związek między nazwą a miejscem przechowywania. Ponadto fakt, że odwołania nie są typami, został wykorzystany do uzasadnienia, dlaczego nie można konstruować typów na podstawie odwołań, mimo że pozwala na to składnia konstrukcji typu. Na przykład tablice lub wskaźniki do odwołań nie są możliwe:const int &ari[5] iconst int &*pri .

Ale w rzeczywistości odwołania typami i dlatego decltype(ri)pobiera węzeł typu referencyjnego, który jest niekwalifikowany. Musisz zejść poza ten węzeł w drzewie typów, aby dostać się do typu bazowego zremove_reference .

Kiedy używasz ri, odniesienie jest rozwiązywane w sposób przezroczysty, więc ri„wygląda i czuje się jak i” i można je nazwać „aliasem”. Jednak w systemie typów riw rzeczywistości istnieje typ, który jest „ odniesieniem do const int ”.

Dlaczego istnieją typy referencyjne?

Weź pod uwagę, że gdyby odwołania nie były typami, wówczas te funkcje zostałyby uznane za mające ten sam typ:

void foo(int);
void foo(int &);

Po prostu nie może tak być z powodów, które są dość oczywiste. Gdyby miały ten sam typ, oznacza to, że każda deklaracja byłaby odpowiednia dla którejkolwiek z definicji, a zatem każda (int)funkcja musiałaby być podejrzewana o przyjmowanie referencji.

Podobnie, gdyby referencje nie były typami, te dwie deklaracje klas byłyby równoważne:

class foo {
  int m;
};

class foo {
  int &m;
};

Byłoby poprawne, gdyby jedna jednostka tłumaczeniowa używała jednej deklaracji, a inna jednostka tłumaczeniowa w tym samym programie używała drugiej deklaracji.

Faktem jest, że odniesienie implikuje różnicę w implementacji i nie można go oddzielić od typu, ponieważ typ w C ++ ma do czynienia z implementacją encji: jej „układ” w bitach, że tak powiem. Jeśli dwie funkcje mają ten sam typ, można je wywołać z tymi samymi konwencjami wywoływania binarnego: ABI jest taki sam. Jeśli dwie struktury lub klasy mają ten sam typ, ich układ jest taki sam, jak również semantyka dostępu do wszystkich członków. Obecność odniesień zmienia te aspekty typów, więc włączenie ich do systemu typów jest prostą decyzją projektową. (Należy jednak zwrócić uwagę na kontrargument: może to być element członkowski struktury / klasy static, który również zmienia reprezentację; ale to nie jest typ!)

Zatem odniesienia znajdują się w systemie typów jako „obywatele drugiej kategorii” (podobnie jak funkcje i tablice w ISO C). Są pewne rzeczy, których nie możemy „zrobić” z odniesieniami, takie jak deklarowanie wskaźników do referencji lub ich tablic. Ale to nie znaczy, że nie są typami. Po prostu nie są typami w sensie, który ma sens.

Nie wszystkie te ograniczenia drugiej klasy są niezbędne. Biorąc pod uwagę, że istnieją struktury odwołań, mogą istnieć tablice odwołań! Na przykład

// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };

// ar[0] is now an alias for x: could be useful!

To po prostu nie jest zaimplementowane w C ++, to wszystko. Wskaźniki do odwołań w ogóle nie mają jednak sensu, ponieważ wskaźnik podniesiony z odniesienia po prostu kieruje się do obiektu, do którego się odwołuje. Prawdopodobnym powodem braku tablic odniesień jest to, że ludzie C ++ uważają tablice za rodzaj funkcji niskiego poziomu dziedziczonej po C, która jest zepsuta na wiele sposobów, które są nieodwracalne i nie chcą dotykać tablic jako podstawa do czegoś nowego. Jednak istnienie tablic odniesień stanowiłoby jasny przykład tego, jak odwołania muszą być typami.

Nie-const -qualifiable typy: znaleziono w ISO C90, też!

Niektóre odpowiedzi sugerują, że odwołania nie wymagają constkwalifikatora. Jest to raczej czerwony śledź, ponieważ deklaracja const int &ri = inawet nie próbuje utworzyć constreferencji kwalifikowanej: jest to odwołanie do typu z kwalifikacją const (który sam w sobie nie jest const). Tak jak const in *rideklaruje wskaźnik do czegoś const, ale ten wskaźnik sam nie jest const.

To powiedziawszy, prawdą jest, że referencje nie mogą constsame nosić kwalifikatora.

Jednak nie jest to takie dziwne. Nawet w języku ISO C 90 nie wszystkie typy mogą być const. Mianowicie, tablic nie może.

Po pierwsze, nie istnieje składnia deklarowania tablicy const: int a const [42]jest błędna.

Jednak to, co stara się zrobić powyższa deklaracja, można wyrazić za pośrednictwem pośrednika typedef:

typedef int array_t[42];
const array_t a;

Ale to nie robi tego, na co wygląda. W tej deklaracji nie kwalifikuje się to, aco jest constkwalifikowane, ale elementy! To znaczy, a[0]jest a const int, ale ajest po prostu „tablicą int”. W konsekwencji nie wymaga to diagnostyki:

int *p = a; /* surprise! */

To robi:

a[0] = 1;

Ponownie, to podkreśla ideę, że odwołania są w pewnym sensie „drugą klasą” w systemie typów, podobnie jak tablice.

Zwróć uwagę, że analogia jest jeszcze głębsza, ponieważ tablice mają również „niewidoczne zachowanie konwersji”, takie jak odwołania. Bez konieczności używania przez programistę żadnego jawnego operatora, identyfikator aautomatycznie zamienia się we int *wskaźnik, tak jakby &a[0]zostało użyte wyrażenie . Jest to analogiczne do tego, jak odniesienie ri, gdy używamy go jako wyrażenia pierwotnego, w magiczny sposób oznacza przedmiot, iz którym jest związane. To tylko kolejny „rozpad”, taki jak „rozpad tablicy na wskaźnik”.

I tak jak nie możemy dać się zmylić faktem, że „tablica do wskaźnika” rozpada się na błędne myślenie, że „tablice są tylko wskaźnikami w C i C ++”, tak samo nie powinniśmy myśleć, że odwołania to tylko aliasy, które nie mają własnego typu.

Gdy decltype(ri)pomija zwykłą konwersję odwołania do obiektu referencyjnego, nie różni się to tak bardzo od sizeof apomijania konwersji tablicy na wskaźnik i operowania na samym typie tablicy w celu obliczenia jej rozmiaru.


Masz tutaj wiele interesujących i pomocnych informacji (+1), ale jest to mniej więcej ograniczone do tego, że „referencje są w systemie typów” - to nie do końca odpowiada na pytanie OP. Pomiędzy tą odpowiedzią a odpowiedzią @ songyuanyao, powiedziałbym, że jest więcej niż wystarczająco informacji, aby zrozumieć sytuację, ale wydaje się, że jest to niezbędne tło dla odpowiedzi, a nie pełna odpowiedź.
Kyle Strand

1
Myślę też, że warto podkreślić to zdanie: „Kiedy używasz ri, odniesienie jest rozwiązane w sposób przejrzysty…” Jeden kluczowy punkt (o którym wspomniałem w komentarzach, ale który jak dotąd nie pojawił się w żadnej z odpowiedzi ) oznacza, że decltype nie wykonuje tej przezroczystej rozdzielczości (nie jest funkcją, więc rinie jest „używany” w opisywanym przez ciebie sensie). Wpisuje się to bardzo ładnie z całym swoim naciskiem na system typu - ich połączenie kluczem jest to, że decltypejest to operacja typu systemu .
Kyle Strand

Hmm, rzeczy o tablicach wydają się być styczne, ale ostatnie zdanie jest pomocne.
Kyle Strand

..... chociaż w tym momencie już cię zagłosowałem i nie jestem OP, więc tak naprawdę niczego nie zyskujesz ani nie tracisz, słuchając mojej krytyki ....
Kyle Strand

Oczywiście łatwa (i poprawna odpowiedź) po prostu wskazuje nam standard, ale podoba mi się tutaj myślenie w tle, chociaż niektórzy mogą twierdzić, że niektóre z nich są przypuszczeniami. +1.
Steve Kidd

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.