Co się stanie, jeśli zdefiniuję tablicę o rozmiarze 0 w C / C ++?

128

Ciekawe, co się właściwie stanie, jeśli zdefiniuję int array[0];w kodzie tablicę o zerowej długości ? GCC w ogóle nie narzeka.

Przykładowy program

#include <stdio.h>

int main() {
    int arr[0];
    return 0;
}

Wyjaśnienie

Właściwie próbuję dowiedzieć się, czy tablice o zerowej długości zainicjowane w ten sposób, zamiast wskazywać na zmienną długość w komentarzach Darhazera, są zoptymalizowane, czy nie.

Dzieje się tak, ponieważ muszę wypuścić jakiś kod na wolność, więc próbuję dowiedzieć się, czy muszę obsługiwać przypadki, w których SIZEzdefiniowano jako 0, co zdarza się w niektórych kodzie ze zdefiniowanym statycznieint array[SIZE];

Zaskoczyło mnie, że GCC nie narzeka, co doprowadziło do mojego pytania. Z otrzymanych odpowiedzi wynika, że ​​brak ostrzeżenia jest w dużej mierze spowodowany obsługą starego kodu, który nie został zaktualizowany nową składnią [].

Ponieważ zastanawiałem się głównie nad błędem, oznaczam odpowiedź Lundina jako poprawną (odpowiedź Nawaza była pierwsza, ale nie była tak kompletna) - inni wskazywali jej faktyczne zastosowanie w konstrukcjach wyściełanych ogonem, chociaż istotne, nie jest dokładnie to, czego szukałem.

Alex Koay
źródło
51
@AlexanderCorwin: Niestety w C ++, z niezdefiniowanym zachowaniem, niestandardowymi rozszerzeniami i innymi anomaliami, samodzielne wypróbowanie czegoś często nie jest drogą do wiedzy.
Benjamin Lindley
5
@JustinKirk Po prostu zostałem przez to uwięziony, testując i sprawdzając, jak działa. A dzięki krytyce, jaką otrzymałem w swoim poście, dowiedziałem się, że testowanie i widzenie, że działa, nie oznacza, że ​​jest ważny i legalny. Dlatego czasami autotest nie jest ważny.
StormByte
2
@JustinKirk, zobacz odpowiedź Matthieu , aby zapoznać się z przykładem, gdzie można jej użyć. Może się również przydać w szablonie, w którym rozmiar tablicy jest parametrem szablonu. Przykład w pytaniu jest oczywiście wyrwany z kontekstu.
Mark Ransom
2
@JustinKirk: Jaki jest cel []w Pythonie, a nawet ""w C? Czasami masz funkcję lub makro, które wymaga tablicy, ale nie masz żadnych danych do umieszczenia w niej.
dan04
15
Co to jest „C / C ++”? To są dwa odrębne języki
Lightness Races in Orbit

Odpowiedzi:

89

Tablica nie może mieć zerowego rozmiaru.

ISO 9899: 2011 6.7.6.2:

Jeżeli wyrażenie jest wyrażeniem stałym, powinno mieć wartość większą od zera.

Powyższy tekst jest prawdziwy zarówno dla zwykłej tablicy (akapit 1). W przypadku VLA (tablicy o zmiennej długości) zachowanie jest niezdefiniowane, jeśli wartość wyrażenia jest mniejsza lub równa zero (akapit 5). To jest tekst normatywny w standardzie C. Kompilator nie może zaimplementować go inaczej.

gcc -std=c99 -pedantic daje ostrzeżenie dla przypadku innego niż VLA.

Lundin
źródło
34
„musi faktycznie powodować błąd” - rozróżnienie między „ostrzeżeniami” i „błędami” nie jest rozpoznawane w standardzie (wspomina tylko o „diagnostyce”), a jedyna sytuacja, w której kompilacja musi się zatrzymać [tj. między ostrzeżeniem a błędem] jest napotkaniem #errordyrektywy.
Random832
12
FYI, co do zasady, standardy (C lub C ++) określają tylko, na co kompilatory muszą zezwalać , ale nie określają tego , co muszą zabronić . W niektórych przypadkach stwierdzą, że kompilator powinien wydać „diagnostykę”, ale jest to tak szczegółowe, jak tylko się da. Reszta należy do dostawcy kompilatora. EDYCJA: Co powiedział też Random832.
mcmcc
8
@Lundin "Kompilatorowi nie wolno budować pliku binarnego zawierającego tablice o zerowej długości." Norma absolutnie nic takiego nie mówi . Mówi tylko, że musi wygenerować co najmniej jeden komunikat diagnostyczny, gdy otrzyma kod źródłowy zawierający tablicę ze stałą o zerowej długości wyrażeniem jej rozmiaru. Jedyną okolicznością, w której standard zabrania kompilatorowi budowania pliku binarnego, jest napotkanie #errordyrektywy preprocesora.
Random832
5
@Lundin Generowanie pliku binarnego dla wszystkich poprawnych przypadków spełnia # 1, a generowanie lub nie generowanie pliku binarnego dla nieprawidłowych przypadków nie wpłynie na to. Wydrukowanie ostrzeżenia jest wystarczające dla # 3. To zachowanie nie ma związku z punktem 2, ponieważ standard nie definiuje zachowania tego kodu źródłowego.
Random832
13
@Lundin: Chodzi o to, że twoje stwierdzenie jest błędne; zgodne kompilatory dopuszczone do tworzenia binarnych zawierającej tablice zerowej długości, o ile diagnostyczny jest wydawana.
Keith Thompson,
85

Zgodnie ze standardem nie jest to dozwolone.

Jednak obecną praktyką w kompilatorach C jest traktowanie tych deklaracji jako deklaracji elastycznego elementu tablicy ( FAM ) :

C99 6.7.2.1, §16 : W szczególnym przypadku ostatni element struktury z więcej niż jednym nazwanym elementem może mieć niekompletny typ tablicy; nazywa się to elastycznym składnikiem tablicy.

Standardowa składnia FAM to:

struct Array {
  size_t size;
  int content[];
};

Chodzi o to, że następnie przydzielisz go tak:

void foo(size_t x) {
  Array* array = malloc(sizeof(size_t) + x * sizeof(int));

  array->size = x;
  for (size_t i = 0; i != x; ++i) {
    array->content[i] = 0;
  }
}

Możesz też użyć go statycznie (rozszerzenie gcc):

Array a = { 3, { 1, 2, 3 } };

Jest to również znane jako struktury wyściełane ogonem (termin ten poprzedza publikację C99 Standard) lub struct hack (dzięki Joe Wreschnigowi za wskazanie tego).

Jednak ta składnia została ustandaryzowana (a efekty gwarantowane) dopiero niedawno w C99. Wcześniej stały rozmiar był konieczny.

  • 1 był przenośny, choć był raczej dziwny.
  • 0 był lepszy w wskazywaniu zamiaru, ale nie był zgodny z prawem, jeśli chodzi o Standard i był obsługiwany jako rozszerzenie przez niektóre kompilatory (w tym gcc).

Jednak praktyka wyściełania ogonem opiera się na fakcie, że miejsce do przechowywania jest dostępne (ostrożnie malloc), więc ogólnie nie nadaje się do używania w stosie.

Matthieu M.
źródło
@Lundin: Nie widziałem tutaj żadnego VLA, wszystkie rozmiary są znane w czasie kompilacji. Termin elastycznej tablicy pochodzi z gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Zero-Length.html i kwalifikuje się int content[];tutaj, o ile rozumiem. Ponieważ nie jestem zbyt obeznany z językiem C tej sztuki ... czy mógłbyś potwierdzić, czy moje rozumowanie wydaje się poprawne?
Matthieu M.
@MatthieuM .: C99 6.7.2.1, §16: W szczególnym przypadku ostatni element struktury z więcej niż jednym nazwanym elementem może mieć niekompletny typ tablicy; nazywa się to elastycznym składnikiem tablicy.
Christoph
Ten idiom jest również znany pod nazwą "struct hack" i spotkałem więcej osób zaznajomionych z tą nazwą niż "struktura wyściełana ogonem" (nigdy wcześniej tego nie słyszałem z wyjątkiem być może ogólnego odniesienia do wypełnienia struktury dla przyszłej kompatybilności ABI ) lub „elastyczny element tablicy”, który po raz pierwszy usłyszałem w C99.
1
Użycie tablicy o rozmiarze 1 do włamania do struktury pozwoliłoby uniknąć skrzypienia kompilatorów, ale było tylko „przenośne”, ponieważ twórcy kompilatorów byli na tyle mili, że uznawali takie użycie za faktyczny standard. Gdyby nie zakaz stosowania tablic o rozmiarze zerowym, konsekwentne używanie przez programistów tablic jednoelementowych jako marnego substytutu i historyczne podejście autorów kompilatorów, że powinny one służyć potrzebom programisty, nawet jeśli nie jest to wymagane przez Standard, twórcy kompilatorów mogliby łatwo i pożytecznie zoptymalizować, foo[x]aby foo[0]zawsze foobyło tablicą jednoelementową.
supercat
1
@RobertSsupportsMonicaCellio: Jak wyraźnie pokazano w odpowiedzi, ale na końcu . Umieściłem również na początku wyjaśnienie, aby było jaśniejsze od samego początku.
Matthieu M.
58

W standardowym C i C ++ tablice o rozmiarze zerowym nie są dozwolone.

Jeśli używasz GCC, skompiluj go z -pedanticopcją. Daje ostrzeżenie , mówiąc:

zero.c:3:6: warning: ISO C forbids zero-size array 'a' [-pedantic]

W przypadku C ++ daje podobne ostrzeżenie.

Nawaz
źródło
9
W programie Visual C ++ 2010:error C2466: cannot allocate an array of constant size 0
Oznacz okup
4
-Werror po prostu zamienia wszystkie ostrzeżenia w błędy, co nie naprawia nieprawidłowego zachowania kompilatora GCC.
Lundin
C ++ Builder 2009 również poprawnie wyświetla błąd:[BCC32 Error] test.c(3): E2021 Array must have at least one element
Lundin
1
Zamiast tego -pedantic -Werrormożesz też po prostu zrobić-pedantic-errors
Stephan Dollberg
3
Tablica o rozmiarze zerowym to nie to samo, co tablica o rozmiarze zerowym std::array. (Na marginesie: Pamiętam, ale nie może znaleźć źródło, że Włas zostały rozważone i wyraźnie odrzucił od bycia w C ++).
27

Jest to całkowicie nielegalne i zawsze było, ale wielu kompilatorów zaniedbuje sygnalizowanie błędu. Nie wiem, dlaczego chcesz to zrobić. Jedynym zastosowaniem, o którym wiem, jest wywołanie błędu czasu kompilacji z wartości logicznej:

char someCondition[ condition ];

Jeśli conditionjest fałszem, pojawia się błąd czasu kompilacji. Ponieważ kompilatory na to pozwalają, postanowiłem użyć:

char someCondition[ 2 * condition - 1 ];

Daje to rozmiar 1 lub -1, a nigdy nie znalazłem kompilatora, który zaakceptowałby rozmiar -1.

James Kanze
źródło
To interesujący sposób na wykorzystanie go.
Alex Koay
10
Myślę, że to powszechna sztuczka w metaprogramowaniu. Nie zdziwiłbym się, gdyby implementacje STATIC_ASSERTgo wykorzystały.
James Kanze,
Dlaczego nie tylko:#if condition \n #error whatever \n #endif
Jerfov2,
1
@ Jerfov2, ponieważ stan może nie być znany w czasie wstępnego przetwarzania, tylko w czasie kompilacji
rmeador
9

Dodam, że na ten argument znajduje się cała strona dokumentacji online gcc.

Niektóre cytaty:

Tablice o zerowej długości są dozwolone w GNU C.

W ISO C90 musiałbyś podać zawartość o długości 1

i

Wersje GCC przed 3.0 umożliwiały statyczną inicjalizację tablic o zerowej długości, tak jakby były one elastycznymi tablicami. Oprócz przypadków, które były przydatne, pozwalał również na inicjalizacje w sytuacjach, które mogłyby uszkodzić późniejsze dane

więc możesz

int arr[0] = { 1 };

i bum :-)

xanatos
źródło
Jak mogę zrobić int a[0], to a[0] = 1 a[1] = 2??
Suraj Jain
2
@SurajJain Jeśli chcesz nadpisać swój stos :-) C nie sprawdza indeksu w porównaniu z rozmiarem tablicy, którą piszesz, więc możesz, a[100000] = 5ale jeśli masz szczęście, po prostu zawiesisz swoją aplikację, jeśli masz szczęście: -)
xanatos
Int a [0]; oznacza tablicę zmiennych (tablicę o zerowym rozmiarze), jak mogę ją teraz przypisać
Suraj Jain
@SurajJain Która część „C nie sprawdza indeksu w porównaniu z rozmiarem tablicy, którą piszesz” nie jest jasna? W C nie ma sprawdzania indeksów, możesz pisać po zakończeniu tablicy i zawiesić komputer lub nadpisać cenne fragmenty pamięci. Więc jeśli masz tablicę zawierającą 0 elementów, możesz pisać po zakończeniu 0 elementów.
xanatos
Zobacz ten quora.com/…
Suraj Jain
9

Innym zastosowaniem tablic o zerowej długości jest tworzenie obiektów o zmiennej długości (przed C99). Macierze zerowej długościróżne od elastycznych tablic które [] Nie 0.

Cytat z dokumentu gcc :

Tablice o zerowej długości są dozwolone w GNU C. Są one bardzo przydatne jako ostatni element struktury, która jest tak naprawdę nagłówkiem obiektu o zmiennej długości:

 struct line {
   int length;
   char contents[0];
 };
 
 struct line *thisline = (struct line *)
   malloc (sizeof (struct line) + this_length);
 thisline->length = this_length;

W ISO C99 użyłbyś elastycznego elementu tablicy, który różni się nieco składnią i semantyką:

  • Elastyczne elementy tablicy są zapisywane jako zawartość [] bez 0.
  • Elastyczne elementy składowe tablicy mają niepełny typ, więc operator sizeof nie może być stosowany.

Rzeczywisty przykład to tablice o zerowej długości struct kdbus_itemw kdbus.h (moduł jądra Linuksa).

Książę
źródło
2
IMHO, nie było dobrego powodu, aby Standard zabraniał tablic o zerowej długości; mógłby mieć obiekty o zerowej wielkości, tak samo dobrze jak elementy struktury i traktować je jako elementy void*arytmetyczne (więc dodawanie lub odejmowanie wskaźników do obiektów o zerowej wielkości byłoby zabronione). Podczas gdy elastyczne składowe tablicy są w większości lepsze niż tablice o zerowej wielkości, mogą one również działać jako rodzaj „unii” aliasów bez dodawania dodatkowego poziomu „syntaktycznego” pośrednictwa do tego, co następuje (np. Mając struct foo {unsigned char as_bytes[0]; int x,y; float z;}dostęp do członków x. z...
supercat
... bezpośrednio, bez konieczności mówienia, np. myStruct.asFoo.xitd. Ponadto IIRC, C wrzeszczy przy każdej próbie włączenia elastycznego elementu tablicy do struktury, uniemożliwiając w ten sposób uzyskanie struktury, która zawiera wiele innych elementów elastycznej tablicy o znanej długości zadowolony.
supercat
@supercat dobrym powodem jest zachowanie integralności reguły dotyczącej dostępu do zewnętrznych granic tablicy. Jako ostatni element struktury, elastyczny element tablicy C99 osiąga dokładnie ten sam efekt, co tablica o rozmiarze zerowym GCC, ale bez konieczności dodawania specjalnych przypadków do innych reguł. IMHO to ulepszenie, które sizeof x->contentsjest błędem w ISO C w przeciwieństwie do zwracania 0 w gcc. Tablice o zerowej wielkości, które nie są elementami strukturalnymi, powodują szereg innych problemów.
MM
@MM: Jakie problemy spowodowałyby, gdyby odjęcie dwóch równych wskaźników do obiektu o rozmiarze zerowym zostało zdefiniowane jako dające zero (podobnie jak odjęcie równych wskaźników do dowolnego rozmiaru obiektu), a odjęcie nierównych wskaźników do obiektów o zerowej wielkości zostało zdefiniowane jako uzyskiwanie Nieokreślona wartość? Jeśli Standard określił, że implementacja może pozwolić na osadzenie struktury zawierającej FAM w innej strukturze, pod warunkiem, że następny element w drugiej strukturze jest tablicą z tym samym typem elementu co FAM lub strukturą zaczynającą się od takiej tablicy i pod warunkiem, że ...
supercat
... rozpoznaje FAM jako aliasowanie tablicy (gdyby reguły wyrównywania powodowały, że tablice lądowałyby w różnych przesunięciach, wymagana byłaby diagnostyka), co byłoby bardzo przydatne. W obecnej sytuacji nie ma dobrej metody, która akceptuje wskaźniki do struktur ogólnego formatu struct {int n; THING dat[];}i może pracować z elementami o statycznym lub automatycznym czasie trwania.
supercat
6

Deklaracje tablic o zerowym rozmiarze w strukturach byłyby przydatne, gdyby były dozwolone, a semantyka byłaby taka, że ​​(1) wymuszałyby wyrównanie, ale w przeciwnym razie nie przydzielałyby żadnej spacji, oraz (2) indeksowanie tablicy byłoby uważane za zdefiniowane zachowanie w przypadek, w którym wynikowy wskaźnik znajdowałby się w tym samym bloku pamięci co struktura. Takie zachowanie nigdy nie było dozwolone przez żaden standard C, ale niektóre starsze kompilatory zezwalały na to, zanim stało się standardem dla kompilatorów, zezwalając na niekompletne deklaracje tablic z pustymi nawiasami.

Struct hack, powszechnie implementowany przy użyciu tablicy o rozmiarze 1, jest podejrzany i nie sądzę, aby istniał żaden wymóg, aby kompilatory go powstrzymywały. Na przykład spodziewałbym się, że jeśli kompilator zobaczy int a[1], będzie miał prawo traktować go a[i]jako plik a[0]. Jeśli ktoś próbuje obejść problemy z wyrównaniem struktury, włamać się za pomocą czegoś takiego jak

typedef struct {
  rozmiar uint32_t;
  dane uint8_t [4]; // Użyj czterech, aby dopełnienie nie wpłynęło na rozmiar struktury
}

kompilator może sprytnie i założyć, że rozmiar tablicy naprawdę wynosi cztery:

; Tak jak napisane
  foo = myStruct-> data [i];
; Zgodnie z interpretacją (zakładając sprzęt little-endian)
  foo = ((* (uint32_t *) myStruct-> data) >> (i << 3)) & 0xFF;

Taka optymalizacja może być rozsądna, zwłaszcza jeśli myStruct->datamogłaby zostać załadowana do rejestru w tej samej operacji, co myStruct->size. W standardzie nie znam niczego, co zabraniałoby takiej optymalizacji, chociaż oczywiście złamałby każdy kod, który mógłby oczekiwać dostępu do rzeczy poza czwartym elementem.

supercat
źródło
1
Elastyczny człon Tablica dodano do C99 jako uprawnionego wersji włamania struct
MM
Standard mówi, że dostęp do różnych elementów tablicy nie powoduje konfliktów, co zwykle uniemożliwiałoby tę optymalizację.
Ben Voigt
@BenVoigt: Standard języka C nie określa efektu zapisania bajtu i jednoczesnego odczytu słowa zawierającego słowo, ale 99,9% procesorów określa, że ​​zapis powiedzie się, a słowo będzie zawierało nową lub starą wersję bajt wraz z niezmienioną zawartością pozostałych bajtów. Jeśli kompilator jest przeznaczony dla takich procesorów, jaki byłby konflikt?
supercat
@supercat: Standard języka C gwarantuje, że jednoczesne zapisy do dwóch różnych elementów tablicy nie powodują konfliktów. Więc twój argument, że (czytaj podczas zapisu) działa dobrze, nie jest wystarczający.
Ben Voigt
@BenVoigt: Jeśli fragment kodu miałby np. Pisać do elementów tablicy 0, 1 i 2 w jakiejś kolejności, nie byłoby dozwolone wczytanie wszystkich czterech elementów w długie, zmodyfikowanie trzech i zapisanie wszystkich czterech z powrotem, ale ja myślę, że można by wczytać wszystkie cztery w długie, zmodyfikować trzy, zapisać z powrotem 16-bitowe dolne bity jako krótkie, a bity 16-23 jako bajt. Nie zgodziłbyś się z tym? A kod, który musiał tylko czytać elementy tablicy, mógłby po prostu wczytać je w long i użyć tego.
supercat