Dlaczego C i C ++ obsługują przypisywanie tablic w ramach struktur, ale nie ogólnie?

87

Rozumiem, że członkowskie przypisywanie tablic nie jest obsługiwane, więc następujące elementy nie będą działać:

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

Po prostu zaakceptowałem to jako fakt, stwierdzając, że celem języka jest zapewnienie otwartej struktury i pozwolenie użytkownikowi zdecydować, jak zaimplementować coś, na przykład kopiowanie tablicy.

Jednak działa:

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

Tablica num[3]jest przypisywana według elementów członkowskich z jej instancji w struct1do jej instancji w struct2.

Dlaczego przypisywanie tablic według elementów członkowskich jest obsługiwane w przypadku struktur, ale nie ogólnie?

edycja : komentarz Rogera Pate'a w wątku std :: string in struct - Problemy z kopiowaniem / przypisaniem? wydaje się wskazywać na ogólny kierunek odpowiedzi, ale nie wiem na tyle, aby samemu to potwierdzić.

edycja 2 : Wiele doskonałych odpowiedzi. Wybrałem Luther Blissett , ponieważ głównie zastanawiałem się nad filozoficznymi lub historycznymi uzasadnieniami tego zachowania, ale odniesienie Jamesa McNellisa do powiązanej dokumentacji specyfikacji było również przydatne.

ozmo
źródło
6
Robię, żeby to miało zarówno C, jak i C ++ jako tagi, ponieważ pochodzi z C. Również dobre pytanie.
GManNickG
4
Warto zauważyć, że dawno temu w C przypisanie struktury nie było generalnie możliwe i trzeba było użyć memcpy()czegoś podobnego.
ggg
Tylko trochę do Twojej wiadomości ... boost::array( boost.org/doc/libs/release/doc/html/array.html ), a teraz std::array( en.cppreference.com/w/cpp/container/array ) są kompatybilnymi z STL alternatywami dla niechlujne stare tablice C. Obsługują przydział kopii.
Emile Cormier
@EmileCormier I są - tada! - struktury wokół tablic.
Peter - Przywróć Monikę

Odpowiedzi:

46

Oto moje spojrzenie na to:

Rozwój języka C oferuje pewien wgląd w ewolucję typu tablic w języku C:

Spróbuję zarysować tablicę:

Prekursory C, B i BCPL, nie miały odrębnego typu tablicy, deklaracja taka jak:

auto V[10] (B)
or 
let V = vec 10 (BCPL)

zadeklarowałby V jako (bez typu) wskaźnik, który jest zainicjowany, aby wskazywać na nieużywany obszar 10 „słów” pamięci. B już używany *dla wskaźnika dereferencing i miał [] zapis krótkiej strony, *(V+i)oznaczałoV[i] , tak jak dzisiaj C / C ++. Jednak Vnie jest tablicą, to nadal jest wskaźnikiem, który musi wskazywać na jakąś pamięć. Spowodowało to problemy, gdy Dennis Ritchie próbował rozszerzyć B za pomocą typów struktur. Chciał, aby tablice były częścią struktur, tak jak dzisiaj w C:

struct {
    int inumber;
    char name[14];
};

Ale z koncepcją B, BCPL tablic jako wskaźników, wymagałoby to, nameaby pole zawierało wskaźnik, który musiałby być zainicjowany w czasie wykonywania do obszaru pamięci 14 bajtów w strukturze. Problem inicjalizacji / układu został ostatecznie rozwiązany przez nadanie tablicom specjalnego traktowania: kompilator śledziłby położenie tablic w strukturach, na stosie itp. Bez konieczności materializacji wskaźnika do danych, z wyjątkiem wyrażeń, które obejmują tablice. To traktowanie pozwoliło na to, aby prawie cały kod B nadal działał i jest źródłem reguły „tablice konwertowane na wskaźnik, jeśli na nie spojrzysz” . Jest to hack kompatybilności, który okazał się bardzo przydatny, ponieważ pozwalał na tablice o otwartym rozmiarze itp.

A oto moje przypuszczenie, dlaczego nie można przypisać tablicy: Ponieważ tablice były wskaźnikami w B, możesz po prostu napisać:

auto V[10];
V=V+5;

aby zmienić bazę „tablicy”. To było teraz bez znaczenia, ponieważ podstawa zmiennej tablicowej nie była już lwartością. Więc to przypisanie było niedozwolone, co pomogło wyłapać kilka programów, które dokonały tego ponownego bazowania na zadeklarowanych tablicach. I wtedy to pojęcie utknęło: ponieważ tablice nigdy nie były zaprojektowane tak, aby były pierwszej klasy cytowane w systemie typu C, były one głównie traktowane jako specjalne bestie, które stały się wskaźnikami, jeśli ich użyjesz. I z pewnego punktu widzenia (który ignoruje fakt, że tablice C to nieudany hack), zakazanie przypisywania tablicy nadal ma sens: otwarta tablica lub parametr funkcji tablicy są traktowane jako wskaźnik bez informacji o rozmiarze. Kompilator nie ma informacji do wygenerowania przypisania tablicy dla nich, a przypisanie wskaźnika było wymagane ze względu na zgodność.

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

Nie zmieniło się to, gdy wersja C w 1978 roku dodała przypisanie struktury ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Mimo że rekordy były odrębnymi typami w C, nie było możliwe przypisanie ich we wczesnym K&R C. Trzeba było je kopiować według elementów członkowskich za pomocą memcpy i można było przekazywać do nich tylko wskaźniki jako parametry funkcji. Przypisanie (i przekazywanie parametrów) zostało teraz po prostu zdefiniowane jako memcpy surowej pamięci struktury, a ponieważ nie mogło to zepsuć istniejącego kodu, zostało łatwo zaadoptowane. Jako niezamierzony efekt uboczny, to pośrednio wprowadziło jakiś rodzaj przypisania tablicy, ale miało to miejsce gdzieś wewnątrz struktury, więc nie mogło to wprowadzić problemów ze sposobem używania tablic.

Nordic Mainframe
źródło
Szkoda, że ​​C nie zdefiniował składni, int[10] c;aby np . Sprawić, by lvalue czachowywała się jak tablica dziesięciu elementów, a nie jako wskaźnik do pierwszego elementu tablicy dziesięciopozycyjnej. Istnieje kilka sytuacji, w których przydatna jest możliwość utworzenia typu typedef, który przydziela miejsce, gdy jest używany dla zmiennej, ale przekazuje wskaźnik, gdy jest używany jako argument funkcji, ale brak możliwości posiadania wartości typu tablicowego jest znaczącą słabością semantyczną w języku.
supercat
Zamiast mówić „wskaźnik, który musi wskazywać na jakąś pamięć”, ważne jest, aby sam wskaźnik był przechowywany w pamięci jak zwykły wskaźnik. Pojawia się to w twoim późniejszym wyjaśnieniu, ale myślę, że to lepiej podkreśla kluczową różnicę. (We współczesnym C nazwa zmiennej tablicowej odnosi się do bloku pamięci, więc to nie jest różnica. Chodzi o to, że sam wskaźnik nie jest logicznie przechowywany nigdzie w abstrakcyjnej maszynie).
Peter Cordes,
Zobacz niechęć C do tablic, aby uzyskać ładne podsumowanie historii.
Peter Cordes
31

Jeśli chodzi o operatory przypisania, standard C ++ mówi, co następuje (C ++ 03 §5.17 / 1):

Istnieje kilka operatorów przypisania ... wszystkie wymagają modyfikowalnej lwartości jako lewego operandu

Tablica nie jest modyfikowalną lwartością.

Jednak przypisanie do obiektu typu klasy jest definiowane specjalnie (§5.17 / 4):

Przypisanie do obiektów klasy jest definiowane przez operator przypisania kopiowania.

Dlatego szukamy, co robi niejawnie zadeklarowany operator przypisania kopii dla klasy (§12.8 / 13):

Operator przypisania kopiowania zdefiniowany niejawnie dla klasy X wykonuje członkowskie przypisanie jej podobiektów. ... Każdy podobiekt jest przypisywany w sposób właściwy dla jego typu:
...
- jeśli podobiekt jest tablicą, to każdy element jest przypisywany w sposób właściwy dla typu elementu
...

Tak więc dla obiektu typu klasy tablice są kopiowane poprawnie. Zwróć uwagę, że jeśli podasz zadeklarowany przez użytkownika operator przypisania kopiowania, nie możesz z tego skorzystać i będziesz musiał skopiować tablicę element po elemencie.


Rozumowanie jest podobne w C (C99 §6.5.16 / 2):

Operator przypisania będzie miał modyfikowalną lwartość jako lewy operand.

Oraz w §6.3.2.1 / 1:

Modyfikowalna lwartość to lwartość, która nie ma typu tablicy ... [po innych ograniczeniach]

W C przypisanie jest znacznie prostsze niż w C ++ (§6.5.16.1 / 2):

W prostym przypisaniu (=) wartość prawego operandu jest konwertowana na typ wyrażenia przypisania i zastępuje wartość przechowywaną w obiekcie wyznaczonym przez lewy operand.

Aby przypisać obiekty typu strukturalnego, lewy i prawy operand muszą mieć ten sam typ, więc wartość prawego operandu jest po prostu kopiowana do lewego operandu.

James McNellis
źródło
1
Dlaczego tablice są niezmienne? A raczej, dlaczego przypisanie nie jest zdefiniowane specjalnie dla tablic, tak jak ma to miejsce w przypadku typu klasowego?
GManNickG,
1
@GMan: To bardziej interesujące pytanie, prawda? W przypadku C ++ odpowiedź prawdopodobnie brzmi „ponieważ tak jest w C”, a dla C, myślę, że wynika to z ewolucji języka (tj. Przyczyna jest historyczna, a nie techniczna), ale ja nie żyłem kiedy większość z nich miała miejsce, więc zostawię to komuś bardziej kompetentnemu, aby odpowiedzieć na tę część :-P (FWIW, nie mogę znaleźć niczego w dokumentach uzasadniających C90 lub C99).
James McNellis
2
Czy ktoś wie, gdzie jest definicja „modyfikowalnej lwartości” w standardzie C ++ 03? To powinno być w §3.10. Indeks mówi, że jest zdefiniowany na tej stronie, ale tak nie jest. (Nienormatywna) uwaga w §8.3.4 / 5 mówi: „Obiekty typów tablic nie mogą być modyfikowane, patrz 3.10”, ale w paragrafie 3.10 ani razu nie używa się słowa „tablica”.
James McNellis
@James: Robiłem to samo. Wydaje się, że odnosi się do usuniętej definicji. I tak, zawsze chciałem poznać prawdziwy powód tego wszystkiego, ale wydaje się to tajemnicą. Słyszałem takie rzeczy jak „zapobieganie nieefektywności ludzi przez przypadkowe przypisywanie tablic”, ale to śmieszne.
GManNickG
1
@GMan, James: Niedawno odbyła się dyskusja na temat comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… jeśli przegapiłeś to i nadal jesteś zainteresowany. Najwyraźniej nie dzieje się tak dlatego, że tablica nie jest modyfikowalną lwartością (tablica z pewnością jest lwartością, a wszystkie wartości inne niż const są modyfikowalne), ale dlatego, że =wymaga rvalue na RHS, a tablica nie może być rvalue ! Konwersja lwartości do rwartości jest zabroniona dla tablic i jest zastępowana lwartością do wskaźnika. static_castnie jest lepszy w tworzeniu wartości r, ponieważ jest zdefiniowana w tych samych warunkach.
Potatoswatter,
2

W tym linku: http://www2.research.att.com/~bs/bs_faq2.html znajduje się sekcja dotycząca przypisywania tablicy:

Oto dwa podstawowe problemy z tablicami

  • tablica nie zna swojego rozmiaru
  • nazwa tablicy jest konwertowana na wskaźnik do jej pierwszego elementu przy najmniejszej prowokacji

Myślę, że to jest podstawowa różnica między tablicami i strukturami. Zmienna tablicowa to element danych niskiego poziomu o ograniczonej wiedzy o sobie. Zasadniczo jest to kawałek pamięci i sposób na indeksowanie.

Tak więc kompilator nie może odróżnić int a [10] i int b [20].

Struktury nie mają jednak tej samej dwuznaczności.

Scott Turley
źródło
3
Ta strona mówi o przekazywaniu tablic do funkcji (czego nie można zrobić, więc jest to tylko wskaźnik, co ma na myśli, gdy mówi, że traci swój rozmiar). Nie ma to nic wspólnego z przypisywaniem tablic do tablic. I nie, zmienna tablicowa to nie tylko „naprawdę” wskaźnik do pierwszego elementu, to tablica. Tablice nie są wskaźnikami.
GManNickG,
Dziękuję za komentarz, ale kiedy czytam tę sekcję artykułu, mówi z góry, że tablice nie znają swojego rozmiaru, a następnie używa przykładu, w którym tablice są przekazywane jako argumenty, aby zilustrować ten fakt. Tak więc, gdy tablice są przekazywane jako argumenty, czy utraciły informacje o swoim rozmiarze, czy też nigdy nie miały informacji na początek. Założyłem to drugie.
Scott Turley
3
Kompilator może powiedzieć różnicę między dwoma różnymi wielkości tablic - spróbuj wydrukować sizeof(a)Vs. sizeof(b)lub przechodząc ado void f(int (&)[20]);.
Georg Fritzsche
Ważne jest, aby zrozumieć, że każdy rozmiar tablicy stanowi swój własny typ. Zasady przekazywania parametrów zapewniają, że można napisać „ogólne” funkcje dla biednych, które pobierają argumenty tablicowe dowolnej wielkości, kosztem konieczności przekazywania rozmiaru osobno. Gdyby tak nie było (a w C ++ można - i trzeba! - definiować parametry referencyjne dla tablic o określonym rozmiarze), potrzebowałbyś specjalnej funkcji dla każdego innego rozmiaru, oczywiście bezsensowne. Pisałem o tym w innym poście .
Peter - Przywróć Monikę
0

Wiem, każdy, kto odpowiedział, jest ekspertem w C / C ++. Ale pomyślałem, to jest główny powód.

num2 = num1;

Tutaj próbujesz zmienić adres bazowy tablicy, co jest niedopuszczalne.

i oczywiście struct2 = struct1;

W tym przypadku obiekt struct1 jest przypisany do innego obiektu.

nsivakr
źródło
A przypisanie struktur ostatecznie przypisuje element tablicy, co nasuwa dokładnie to samo pytanie. Dlaczego jedna jest dozwolona, ​​a druga nie, skoro jest tablicą w obu sytuacjach?
GManNickG
1
Zgoda. Ale kompilator zapobiega pierwszej z nich (num2 = num1). Kompilator nie zapobiega drugiemu. To robi ogromną różnicę.
nsivakr
Gdyby tablice były przypisywalne, num2 = num1zachowywałby się doskonale. Elementy of num2miałyby taką samą wartość jak odpowiedni element num1.
juanchopanza
0

Innym powodem, dla którego nie podjęto dalszych wysiłków w celu ulepszenia tablic w C, jest prawdopodobnie to, że przypisanie tablicy nie byłoby tak przydatne. Mimo że można to łatwo osiągnąć w C, opakowując go w strukturę (a adres struktury można po prostu rzutować na adres tablicy lub nawet na adres pierwszego elementu tablicy do dalszego przetwarzania), ta funkcja jest rzadko używana. Jednym z powodów jest to, że tablice o różnych rozmiarach są niekompatybilne, co ogranicza korzyści wynikające z przypisania lub, powiązanego, przekazywania do funkcji według wartości.

Większość funkcji z parametrami tablicowymi w językach, w których tablice są typami pierwszej klasy, jest napisanych dla tablic o dowolnym rozmiarze. Następnie funkcja zwykle wykonuje iterację po zadanej liczbie elementów, czyli informacjach dostarczanych przez tablicę. (W języku C idiom to oczywiście przekazanie wskaźnika i oddzielnej liczby elementów). Funkcja, która akceptuje tablicę tylko jednego określonego rozmiaru, nie jest potrzebna tak często, więc niewiele brakuje. (Zmienia się to, gdy można pozostawić kompilatorowi wygenerowanie oddzielnej funkcji dla dowolnego występującego rozmiaru tablicy, tak jak w przypadku szablonów C ++; to jest powód, dlaczego std::arrayjest przydatny).

Peter - przywróć Monikę
źródło