Dlaczego tablice odniesień są nielegalne?

149

Poniższy kod nie jest kompilowany.

int a = 1, b = 2, c = 3;
int& arr[] = {a,b,c,8};

Co na ten temat mówi standard C ++?

Wiem, że mógłbym zadeklarować klasę, która zawiera odniesienie, a następnie utworzyć tablicę tej klasy, jak pokazano poniżej. Ale naprawdę chcę wiedzieć, dlaczego powyższy kod się nie kompiluje.

struct cintref
{
    cintref(const int & ref) : ref(ref) {}
    operator const int &() { return ref; }
private:
    const int & ref;
    void operator=(const cintref &);
};

int main() 
{
  int a=1,b=2,c=3;
  //typedef const int &  cintref;
  cintref arr[] = {a,b,c,8};
}

Możliwe jest użycie struct cintrefzamiast const int &symulacji tablicy odniesień.

Alexey Malistov
źródło
1
Nawet jeśli tablica była prawidłowa, przechowywanie w niej surowej wartości „8” nie zadziała. Gdybyś zrobił "intlink value = 8;", zginąłby strasznie, ponieważ prawie właśnie został przetłumaczony na "const int & value = 8;". Odniesienie musi odwoływać się do zmiennej.
Grant Peters
3
intlink value = 8;działa. sprawdź, czy nie wierzysz.
Alexey Malistov
7
Jak wskazuje Alexey, wiązanie wartości r z odniesieniem do stałej jest całkowicie poprawne.
avakar
1
Ale to nie działa operator=. Nie można ponownie umieścić referencji. Jeśli naprawdę chcesz takie semantykę - choć nie osobiście znaleźć sytuację, w której jesteśmy praktycznie użyteczne - to std::reference_wrapperbyłby sposób, aby to zrobić, jak to faktycznie przechowuje wskaźnik ale zapewnia odniesienie podobny operators i nie pozwalają ponownym zainstalowaniu. Ale wtedy użyłbym tylko wskaźnika!
underscore_d
1
Operator = jest prywatny i nie zaimplementowany, czyli w C ++ 11 jest to = delete.
Jimmy Hartzell

Odpowiedzi:

148

Odpowiadając na Twoje pytanie o standard mogę przytoczyć C ++ Standard §8.3.2 / 4 :

Nie będzie żadnych odniesień do odniesień, żadnych tablic odniesień ani wskaźników do odniesień.

Kirill V. Lyadvinsky
źródło
32
Cóż więcej można powiedzieć?
polyglot
9
powinny istnieć tablice odniesień z tego samego powodu, dla którego mamy tablice wskaźników, te same informacje, ale inną obsługę, prawda?
neu-rah
6
Gdyby programiści kompilatora zdecydowali, jako rozszerzenie, aby zezwolić na tablice odniesień, czy byłoby to sukcesem? A może są jakieś prawdziwe problemy - być może niejednoznaczny kod - które sprawiłyby, że zdefiniowanie tego rozszerzenia byłoby zbyt cholernie skomplikowane?
Aaron McDaid
23
@AaronMcDaid Myślę, że prawdziwym powodem - który jest tak ewidentnie nieobecny w tej i podobnych dyskusjach - jest to, że gdyby ktoś miał tablicę odniesień, jak można by rozróżnić między adresem elementu a adresem jego odniesienia? Natychmiastowy sprzeciw wobec „Nie można umieścić odniesienia w tablicy / kontenerze / czymkolwiek” jest taki, że można wstawić element, structktórego jedyny element członkowski jest referencją. Ale robiąc to, masz teraz zarówno odniesienie, jak i obiekt nadrzędny do nazwania ... co teraz oznacza, że ​​możesz jednoznacznie określić, który adres chcesz. Wydaje się oczywiste ... więc dlaczego nikt tego nie powiedział?
underscore_d
95
@polyglot What more is there to say?A uzasadnienie dlaczego ? Chodzi mi wyłącznie o Standard, ale samo jego cytowanie i założenie, że to koniec dyskusji wydaje się pewnym sposobem na zniszczenie krytycznej myśli użytkowników - wraz z wszelkimi możliwościami ewolucji języka, np. Poza ograniczeniami pominięcia lub sztuczne ograniczenie. Jest ich za dużo ”, ponieważ norma mówi„ tutaj - i za mało ”i bardzo rozsądne jest to powiedzieć z następujących powodów praktycznych”. Nie wiem, dlaczego głosy za napływają tak chętnie do odpowiedzi, które mówią tylko o pierwszym, nawet nie próbując zgłębiać drugiego.
underscore_d
64

Referencje nie są obiektami. Nie mają własnego magazynu, po prostu odwołują się do istniejących obiektów. Z tego powodu tablice odwołań nie mają sensu.

Jeśli potrzebujesz lekkiego obiektu, który odwołuje się do innego obiektu, możesz użyć wskaźnika. Będziesz mógł używać elementu structreferencyjnego jako obiektów w tablicach tylko wtedy, gdy podasz jawną inicjalizację dla wszystkich członków odniesienia dla wszystkich structinstancji. Odwołania nie mogą być inicjalizowane domyślnie.

Edycja: Jak zauważa jia3ep, w standardowej sekcji dotyczącej deklaracji istnieje wyraźny zakaz dotyczący tablic odniesień.

CB Bailey
źródło
6
Odniesienia mają taką samą naturę jak stałe wskaźniki, dlatego zajmują trochę pamięci (miejsca), aby wskazywać na coś.
inazaruk
7
Niekoniecznie. Kompilator może być w stanie uniknąć przechowywania adresu, jeśli jest to na przykład odniesienie do obiektu lokalnego.
EFraim
4
Niekoniecznie. Referencje w strukturach zwykle zajmują trochę miejsca. Lokalne odniesienia często nie. Tak czy inaczej, w ścisłym standardowym sensie odniesienia nie są obiektami i (to było dla mnie nowością) nazwane odniesienia nie są w rzeczywistości zmiennymi .
CB Bailey
26
Tak, ale to jest szczegół implementacji. Obiekt w modelu C ++ jest wpisany region przechowywania. Odniesienie wyraźnie nie jest obiektem i nie ma gwarancji, że zajmie miejsce w jakimkolwiek konkretnym kontekście.
CB Bailey
9
Ujmę to tak: ty może mieć struct tylko o 16 pozycje literaturowe do Foo i używać go w dokładnie taki sam sposób, jak chciałbym używać mojego wachlarz literatury - oprócz tego, że nie można do indeksu odniesienia foo 16 . To jest brodawka językowa IMHO.
greggo
28

To interesująca dyskusja. Oczywiście tablice referencji są całkowicie nielegalne, ale IMHO powód, dla którego nie jest tak prosty, jak powiedzenie „nie są obiektami” lub „nie mają rozmiaru”. Zwróciłbym uwagę, że same tablice nie są pełnoprawnymi obiektami w C / C ++ - jeśli sprzeciwiasz się temu, spróbuj utworzyć instancję niektórych klas szablonów stl, używając tablicy jako parametru szablonu „class” i zobacz, co się stanie. Nie możesz ich zwrócić, przypisać, przekazać jako parametry. (parametr tablicy jest traktowany jako wskaźnik). Ale tworzenie tablic tablic jest legalne. Referencje mają rozmiar, który kompilator może i musi obliczyć - nie możesz sizeof () odwołania, ale możesz utworzyć strukturę zawierającą tylko odwołania. Będzie miał wystarczający rozmiar, aby pomieścić wszystkie wskaźniki, które implementują odwołania. Możesz'

struct mys {
 int & a;
 int & b;
 int & c;
};
...
int ivar1, ivar2, arr[200];
mys my_refs = { ivar1, ivar2, arr[12] };

my_refs.a += 3  ;  // add 3 to ivar1

W rzeczywistości możesz dodać tę linię do definicji struktury

struct mys {
 ...
 int & operator[]( int i ) { return i==0?a : i==1? b : c; }
};

... a teraz mam coś, co wygląda DUŻO jak tablica referencji:

int ivar1, ivar2, arr[200];
mys my_refs = { ivar1, ivar2, arr[12] };

my_refs[1] = my_refs[2]  ;  // copy arr[12] to ivar2
&my_refs[0];               // gives &my_refs.a == &ivar1

Otóż, to nie jest prawdziwa tablica, to przeciążenie operatora; nie zrobi rzeczy, które normalnie robią tablice, na przykład sizeof (arr) / sizeof (arr [0]). Ale robi dokładnie to, czego chcę, aby robił szereg odniesień, z całkowicie legalnym C ++. Z wyjątkiem (a) uciążliwa jest konfiguracja dla więcej niż 3 lub 4 elementów i (b) wykonuje obliczenia przy użyciu zestawu?: Co można zrobić za pomocą indeksowania (nie przy normalnym indeksowaniu semantyki wskaźnika C , ale mimo to indeksowanie). Chciałbym zobaczyć bardzo ograniczony typ „tablicy referencyjnej”, który faktycznie może to zrobić. To znaczy tablica odniesień nie byłaby traktowana jako ogólna tablica rzeczy, które są referencjami, ale raczej byłaby nową „tablicą referencji” zrobić za pomocą szablonów).

to prawdopodobnie zadziała, jeśli nie masz nic przeciwko temu rodzajowi nieprzyjemności: przetwórz '* this' jako tablicę int * i zwróć referencję utworzoną z jednego: (niezalecane, ale pokazuje jak właściwa 'tablica' pracowałbym):

 int & operator[]( int i ) { return *(reinterpret_cast<int**>(this)[i]); }
greggo
źródło
1
Możesz to zrobić w C ++ 11 std::tuple<int&,int&,int&> abcref = std::tie( a,b,c)- który tworzy „tablicę” referencji, które mogą być indeksowane tylko przez stałe czasu kompilacji przy użyciu std :: get. Ale nie możesz tego zrobić std::array<int&,3> abcrefarr = std::tie( a,b,c). Może jest na to sposób, o którym nie wiem. Wydaje mi się, że powinien istnieć sposób na napisanie specjalizacji, std::array<T&,N>która może to zrobić - a więc funkcja, std::tiearray(...)która zwraca jedną taką (lub zezwala std::array<T&,N>na akceptację inicjatora zawierającego adresy = {& v1, & v2 itd.})
greggo
Twoja propozycja jest głupia (IMO), a twoja ostatnia linia zakłada, że ​​int może zawierać wskaźnik (zwykle fałsz, przynajmniej tam, gdzie pracuję). Ale jest to jedyna odpowiedź z uzasadnionym uzasadnieniem, dlaczego tablice odniesień są zabronione, więc +1
Nemo
@Nemo nie było to tak naprawdę pomyślane jako propozycja, ale tylko po to, aby zilustrować, że funkcjonalność takiej rzeczy nie jest absurdalna. W moim ostatnim komentarzu zauważyłem, że c ++ ma rzeczy, które są bliskie. Ponadto ostatnia linia, do której się odwołujesz, zakłada, że ​​pamięć implementująca odwołanie do int może być użytecznie aliasowana jako wskaźnik do int - struktura zawiera odwołania, a nie int - i to zwykle jest prawdą. Nie twierdzę, że jest przenośny (ani nawet bezpieczny przed regułami „niezdefiniowanego zachowania”). miało to zilustrować, jak indeksowanie można wykorzystać pod maską, aby uniknąć wszystkich?:
greggo
Słusznie. Ale uważam, że ten pomysł jest „absurdalny” i właśnie dlatego tablice odniesień są niedozwolone. Tablica to przede wszystkim kontener. Wszystkie kontenery wymagają typu iteratora, aby można było zastosować standardowe algorytmy. Typ iteratora dla „tablicy T” to zwykle „T *”. Jaki byłby typ iteratora dla tablicy odniesień? Ilość włamań językowych potrzebnych do rozwiązania wszystkich tych problemów jest absurdalna jak na zasadniczo bezużyteczną funkcję. Właściwie może uczynię to własną odpowiedzią :-)
Nemo
@Nemo Nie musi być częścią języka podstawowego, więc typ iteratora będący „niestandardowym” nie jest wielkim problemem. Wszystkie różne typy kontenerów mają swoje typy iteratorów. W tym przypadku iterator, wyłuskany, będzie odnosił się do obiektów docelowych, a nie do referencji w tablicy, co jest w pełni zgodne z zachowaniem tablicy. Może nadal wymagać trochę nowego zachowania C ++ do obsługi jako szablon Bardzo prosty std::array<T,N>, łatwy do zdefiniowania w kodzie aplikacji, nie został dodany do std :: do C ++ 11, prawdopodobnie dlatego, że warto mieć to bez możliwości zrobić = {1,2,3};Czy to „hacking językowy”?
greggo
28

Komentarz do Twojej zmiany:

Lepszym rozwiązaniem jest std::reference_wrapper.

Szczegóły: http://www.cplusplus.com/reference/functional/reference_wrapper/

Przykład:

#include <iostream>
#include <functional>
using namespace std;

int main() {
    int a=1,b=2,c=3,d=4;
    using intlink = std::reference_wrapper<int>;
    intlink arr[] = {a,b,c,d};
    return 0;
}
Youw
źródło
Cześć, zostało to powiedziane w 09. Porada, szukaj nowszych postów :)
Stígandr
9
Post jest stary, ale nie wszyscy wiedzą o rozwiązaniu z reference_wrapper. Ludzie muszą przeczytać ruch o STL i STDlib w C ++ 11.
Youw
Daje to link i przykładowy kod, a nie tylko podaje nazwę klasy reference_wrapper.
David Stone
12

Tablica jest niejawnie konwertowana na wskaźnik, a wskaźnik do odwołania jest niedozwolony w C ++

EFraim
źródło
11
Prawdą jest, że nie możesz mieć wskaźnika do referencji, ale to nie jest powód, dla którego nie możesz mieć tablicy referencji. Raczej oba są symptomami tego, że odniesienia nie są przedmiotami.
Richard Corden
1
Struktura może zawierać tylko odwołania i będzie miała rozmiar proporcjonalny do ich liczby. Gdybyś miał tablicę referencji „arr”, normalna konwersja tablicy do wskaźnika nie miałaby sensu, ponieważ „wskaźnik do odniesienia” nie ma sensu. ale sensowne byłoby użycie po prostu arr [i], w taki sam sposób jak st.ref, gdy „st” jest strukturą zawierającą ref. Hmm. Ale & arr [0] dałoby adres pierwszego obiektu, do którego się odwołujemy, a & arr [1] - & arr [0] nie byłoby tym samym, co & arr [2] - & arr [1] - spowodowałoby to wiele dziwności.
greggo
11

Biorąc pod uwagę int& arr[] = {a,b,c,8};, co to jest sizeof(*arr)?

Wszędzie indziej odniesienie jest traktowane jako po prostu samo w sobie, więc sizeof(*arr)powinno po prostu być sizeof(int). Ale to spowodowałoby, że arytmetyka wskaźnika tablicy na tej tablicy byłaby nieprawidłowa (zakładając, że odwołania nie mają takich samych szerokości, to ints). Aby wyeliminować niejednoznaczność, jest to zabronione.

PaulMurrayCbr
źródło
Tak. Nie możesz mieć tablicy czegoś, chyba że ma rozmiar. Jaka jest wielkość referencji?
David Schwartz
4
@DavidSchwartz Utwórz, structktórego jedynym członkiem jest ta referencja i dowiedz się ..?
underscore_d
3
To powinna być akceptowana odpowiedź, a nie ta głupia pedantyczna, która po prostu rzuca standardem w PO.
Tyson Jacobs
Może jest literówka. „Zakładając, że odwołania nie mają takich samych szerokości, to int”, powinno być „zakładając, że odwołania nie mają takich samych szerokości jak int”. Zmiana ma na co .
zhenguoli
Ale czekaj, zgodnie z tą logiką nie możesz mieć zmiennych składowych odniesienia.
Tyson Jacobs
10

Ponieważ, jak wielu tutaj powiedziało, odniesienia nie są obiektami. są po prostu aliasami. To prawda, że ​​niektóre kompilatory mogą zaimplementować je jako wskaźniki, ale standard tego nie wymusza / nie określa. A ponieważ odniesienia nie są obiektami, nie możesz ich wskazać. Przechowywanie elementów w tablicy oznacza, że ​​istnieje pewien rodzaj adresu indeksu (tj. Wskazujący na elementy o określonym indeksie); i dlatego nie możesz mieć tablic odniesień, ponieważ nie możesz ich wskazać.

Zamiast tego użyj boost :: reference_wrapper lub boost :: tuple; lub po prostu wskazówki.

nawigator
źródło
5

Uważam, że odpowiedź jest bardzo prosta i ma związek z semantycznymi regułami odwołań i sposobem obsługi tablic w C ++.

W skrócie: referencje można traktować jako struktury, które nie mają domyślnego konstruktora, więc obowiązują wszystkie te same reguły.

1) Semantycznie odwołania nie mają wartości domyślnej. Referencje można tworzyć tylko poprzez odwoływanie się do czegoś. Referencje nie mają wartości reprezentującej brak odniesienia.

2) Podczas przydzielania tablicy o rozmiarze X program tworzy kolekcję obiektów inicjalizowanych domyślnie. Ponieważ odwołanie nie ma wartości domyślnej, utworzenie takiej tablicy jest semantycznie niedozwolone.

Ta reguła dotyczy również struktur / klas, które nie mają domyślnego konstruktora. Poniższy przykład kodu nie kompiluje się:

struct Object
{
    Object(int value) { }
};

Object objects[1]; // Error: no appropriate default constructor available
Kristupas A.
źródło
1
Można utworzyć tablicę Object, po prostu trzeba się upewnić, aby je wszystkie zainicjować: Object objects[1] = {Object(42)};. W międzyczasie nie możesz utworzyć tablicy odniesień, nawet jeśli zainicjujesz je wszystkie.
Anton3
4

Dzięki tej strukturze szablonu możesz podejść dość blisko. Musisz jednak zainicjować wyrażeniami, które są wskaźnikami do T, a nie T; i tak, chociaż możesz w podobny sposób łatwo utworzyć 'fake_constref_array', nie będziesz w stanie powiązać tego z wartościami r, jak to zrobiono w przykładzie OP ('8');

#include <stdio.h>

template<class T, int N> 
struct fake_ref_array {
   T * ptrs[N];
  T & operator [] ( int i ){ return *ptrs[i]; }
};

int A,B,X[3];

void func( int j, int k)
{
  fake_ref_array<int,3> refarr = { &A, &B, &X[1] };
  refarr[j] = k;  // :-) 
   // You could probably make the following work using an overload of + that returns
   // a proxy that overloads *. Still not a real array though, so it would just be
   // stunt programming at that point.
   // *(refarr + j) = k  
}

int
main()
{
    func(1,7);  //B = 7
    func(2,8);     // X[1] = 8
    printf("A=%d B=%d X = {%d,%d,%d}\n", A,B,X[0],X[1],X[2]);
        return 0;
}

-> A = 0 B = 7 X = {0,8,0}

greggo
źródło
2

Obiekt referencyjny nie ma rozmiaru. Jeśli napiszesz sizeof(referenceVariable), poda on rozmiar obiektu, do którego się odwołujesz referenceVariable, a nie samego odniesienia. Nie ma własnego rozmiaru, dlatego kompilator nie może obliczyć rozmiaru wymaganego przez tablicę.

Carl Seleborg
źródło
3
jeśli tak, to jak kompilator może obliczyć rozmiar struktury zawierającej tylko ref?
Juster
Kompilator zna fizyczny rozmiar odwołania - jest taki sam jak wskaźnik. Odniesienia są w istocie gloryfikowanymi semantycznie wskaźnikami. Różnica między wskaźnikami a odwołaniami polega na tym, że odwołania mają sporo reguł semantycznych umieszczonych na nich, aby zmniejszyć liczbę błędów, które możesz utworzyć w porównaniu ze wskaźnikami.
Kristupas A.
2

Kiedy przechowujesz coś w tablicy, jego rozmiar musi być znany (ponieważ indeksowanie tablicy zależy od rozmiaru). Wg standardu C ++ Nie określono, czy referencja wymaga przechowywania, czy też nie, ponieważ indeksowanie tablicy referencji nie byłoby możliwe.

Pradyot
źródło
1
Ale umieszczając wspomniane odniesienie w struct, magicznie wszystko staje się określone, dobrze zdefiniowane, gwarantowane i ma deterministyczny rozmiar. Dlaczego zatem istnieje ta pozornie całkowicie sztuczna różnica?
underscore_d
... oczywiście, jeśli faktycznie zacznie się myśleć o tym, co structzapewnia opakowanie , zaczną nasuwać się dobre możliwe odpowiedzi - ale dla mnie nikt jeszcze tego nie zrobił, mimo że jest to jedno z bezpośrednich wyzwań dla wszystkich powszechnych uzasadnienia, dlaczego nie można używać nieopakowanych referencji.
underscore_d
2

Wystarczy dodać do całej rozmowy. Ponieważ tablice wymagają kolejnych lokalizacji pamięci do przechowywania elementu, więc jeśli utworzymy tablicę referencji, nie ma gwarancji, że będą one znajdować się w kolejnych lokalizacjach pamięci, więc dostęp będzie problemem i dlatego nie możemy nawet zastosować wszystkich operacji matematycznych na szyk.

Amit Kumar
źródło
Rozważ poniższy kod z pierwotnego pytania: int a = 1, b = 2, c = 3; int & arr [] = {a, b, c, 8}; Jest znacznie mniejsze prawdopodobieństwo, że a, b, c będą znajdować się w kolejnym miejscu pamięci.
Amit Kumar
To jest dalekie od głównego problemu z koncepcją przechowywania referencji w kontenerze.
underscore_d
0

Rozważ tablicę wskaźników. Wskaźnik to tak naprawdę adres; więc kiedy inicjalizujesz tablicę, analogicznie mówisz komputerowi: „przydziel ten blok pamięci do przechowywania tych liczb X (które są adresami innych elementów)”. Następnie, jeśli zmienisz jeden ze wskaźników, zmieniasz tylko to, na co on wskazuje; nadal jest to adres numeryczny, który sam znajduje się w tym samym miejscu.

Odwołanie jest analogiczne do aliasu. Gdybyś miał zadeklarować tablicę odniesień, po prostu powiedziałbyś komputerowi: „przydziel tę amorficzną plamę pamięci składającą się z tych wszystkich różnych elementów rozrzuconych dookoła”.

Dan Tao
źródło
1
Dlaczego więc utworzenie, structktórego jedyny członek jest odniesieniem, prowadzi do czegoś całkowicie legalnego, przewidywalnego i nieamorficznego? Dlaczego użytkownik musi zawijać odniesienie, aby w magiczny sposób uzyskać moc obsługującą tablicę, czy też jest to tylko sztuczne ograniczenie? IMO żadna odpowiedź nie dotyczyła tego bezpośrednio. Zakładam, że obalenie brzmi: „Obiekt opakowujący zapewnia, że można użyć semantyki wartości w postaci obiektu opakowującego, dzięki czemu można mieć gwarancje wartości, które są potrzebne, ponieważ np. W jaki sposób można rozróżnić element i adres referencyjny” ... Czy to miałeś na myśli?
underscore_d
-2

Właściwie jest to mieszanka składni C i C ++.

Powinieneś albo używać czystych tablic C, które nie mogą być referencjami, ponieważ referencje są tylko częścią C ++. Lub pójdziesz drogą C ++ i użyjesz klasy std::vectorlub std::arraydo swoich celów.

Jeśli chodzi o część edytowaną: mimo że structjest elementem z C, definiujesz konstruktora i funkcje operatora, co czyni go C ++ class. W związku z structtym nie można kompilować w czystym C!

Thargon
źródło
O co ci chodzi? std::vectorlub też std::arraynie może zawierać odniesień. Standard C ++ jest dość wyraźny, że żaden kontener nie może.
underscore_d