Cześć,
Dostałem linka do tego wątku, i po jego lekturze stwierdziłem, że przesiedzenie 2h nad standardem w ramach zabicia nudy w pociągu brzmi kusząco (na szczęście w shinkansenie pomiędzy Kyoto a Tokyo jest net przez całą drogę).
TL;DR: wg moich ustaleń @Endrju ma rację wg standardu i to jest UB (nie patrzyłem na to, czy popularne implementacje to dodefiniowują, co mogą robić btw; skupiłem się głównie na C++14).
Troche notatek z tego na co patrzyłem:
Zacząłem od przeczytania tego wątku ze trzy razy, plus fragmentu standardu C++14 i C11, który @Endrju podlinkował. W tym ostatnim najciekawsze jest to zdanie (zresztą już cytowane przez @pingwindyktator ):
If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.
Tu pojawiły się dwa pytania - co to jest "array" i co to jest "object", i jak to się ma do dwu-/wielowymiarowych tablic.
"Object" jest wyjasniany 1.8; ciekawe są dwa fragmenty:
C++14 1.8/2 [intro.object]
Objects can contain other objects, called subobjects. A subobject can be a member subobject (9.2), a base class subobject (Clause 10), or an array element. An object that is not a subobject of any other object is called a complete object
C++14 1.8/3 [intro.object]
For every object x, there is some object called the complete object of x, determined as follows:
— If x is a complete object, then x is the complete object of x.
— Otherwise, the complete object of x is the complete object of the (unique) object that contains x
Przenosząc powyższe na przykład tablicy dwu-wymiarowej int a[2][3]
można powiedzieć, że:
- "a" jest "kompletnym obiektem" (sry za słabe tłumaczenie; na potrzeby dyskusji wystarczy), który zawiera N "podobiektów" (konkretniej dwa, ale o tym czemu dwa to za chwilę);
- oba "podobiekty" zawierają po trzy swoje "podobiekty" typu int
Czyli wg. tego można by powiedzieć zarówno, że jest jakaś hierarchia obiektów, jak i że a[0][0] jest elementem tego samego "kompletnego obiektu" bo a[1][1]. Należy zaznaczyć, że to jednak nie oznacza, że są elementem tej samej tablicy ("array") - nie można pociągnąć implikacji w tą stronę.
"Array" jest wyjaśniony natomiast w 8.3.4; ciekawe (wg mnie) są następujące fragmenty:
C++14 8.3.4/3 [dcl.array]
When several “array of” specifications are adjacent, a multidimensional array is created. [...]
Tutaj standard wprowadza pojęcie "multidimensional array". Dalej:
C++14 8.3.4/7 [dcl.array]
[ Note: Except where it has been declared for a class (13.5.5), the subscript operator [] is interpreted in such a way that E1[E2] is identical to *((E1)+(E2)). [...]
Powyższy cytat jest istotny aby wykazać powiązanie tego na co @Endrju się powołał z samymi arrayami.
Idąc dalej, bardzo ciekawy jest następujący przykład:
C++14 8.3.4/5 [dcl.array]
[ Example: [...]
static int x3d[3][5][7];
declares a static three-dimensional array of integers, with rank 3×5×7. In complete detail, x3d is an array of three items; each item is an array of five arrays; each of the latter arrays is an array of seven integers.
A więc explicit jest napisane, że jest to tablica tablic tablic. Nie jest natomiast nigdzie w okolicy napisane, że tablica tablic (aka tablica wielowymiarowa) jest:
- kompatybilna z tablicą jednowymiarową o tej samej wielkości (w rozumieniu storage space)
- rzutowalna pomiędzy takimi tablicami
- pointery na takie tablice są kompatybilne (tj. można zgodnie ze standardem przerzutować między nimi; można to zapisać w języku oczywiście, jak wiele innych UB)
- Nie jest również napisane, że istnieje coś takiego jak "complete array" (analogicznie do "complete object") który miałby "sub-arraye" (analogiczne do "sub-objects").
Jednocześnie znalazłem dwie inne informacje, że gdyby coś takiego było, to by miało prawo działać:
- ułożenie elementów w tablicy, jeden po drugim, jest gwarantowane przez standard ("An object of array type contains a contiguously allocated non-empty set of N subobjects of type T" C++14 8.3.4)
- żadne z "sub-arrayów" (to mój termin, nie standardu) nie może mieć innego alignmentu niż domyślny (to wynika z zasad związanych z sizeof, na co podpowiedź znalazłem na stackoverflow i o czym faktycznie pisze w standardzie (5.3.3/2 - "When applied to an array, the result is the total number of bytes in the array. This implies that the size of an array of n elements is n times the size of an element." - alignment sub-arraya by sprawił, że sizeof zwróciłby nieprawdziwy wynik, więc tak się stać nie może, bo to by było niezgodne ze standardem)
Mniej więcej w tym momencie byłem przekonany, że słowo "array" w oryginalnym cytacie dotyczy tylko i wyłącznie tego konkretnego arraya, na podstawie którego został stworzony pointer, który sobie po nim biega. Nie dotyczy natomiast "kompletnego obiektu" (ponieważ nie istnieje coś takiego jak "kompletna tablica" w standardzie - jest tylko kompletny obiekt, który jest dużo bardziej ogólnym terminem).
Niemniej jednak jak wiemy, częsty wzorzec jaki się widzi, to przekazywanie tablicy dwu-wymiarowej przez pointer na jej pierwszy element i dwie wielkości (szerokość i wysokość) - ba, jestem pewien, że gdyby moje stare posty odnaleźć, to też o tym pisałem jako o stosowanym rozwiązaniu.
Zacząłem więc sprawdzać, czy jest możliwe przekazanie dwu-wymiarowej tablicy do funkcji nie korzystając z tego tricku (jeśli by nie było możliwe, to może jednak moje przekonanie byłoby błędne).
Oczywiście jak najbardziej jest możliwe - wstawienie nazwy wielowymiarowego arraya jako parametr funkcji wstawia tak naprawdę adres jej pierwszego elementu (array-to-pointer implicit cast się kłania), który jest typu tablica tablic (o jeden wymiar mniejsza), np. (kudos bot, który @kq postawił):
<@Gynvael> cxx: template<typename T> void f(T x) { cout << typeid(x).name(); } int main() { int a[2][3]; f(a); }
< cxx> int (*) [3]
Kolejne pytanie, które się pojawiło, to "czemu u licha używa się rozwiązania, które jest UB, do pracy na tablicach wielowymiarowych?" oraz "czemu nikt się nie skapnął, że to UB?!?!".
Po wrzuceniu kilku zapytań w google wpadł mi fajny link z comp.lang.c FAQ - http://c-faq.com/aryptr/ary2dfunc2.html:
It must be noted, however, that a program which performs multidimensional array subscripting ``by hand'' in this way is not in strict conformance with the ANSI C Standard; according to an official interpretation, the behavior of accessing (&array[0][0])[x] is not defined for x >= NCOLUMNS.
Co jest zgodne z moimi ustaleniami.
Podsumowując to wall-of-text. Owszem, jest to niezgodne ze standarem C++14 i, jeśli wierzyć comp.lang.C FAQ, to przynajmniej z ANSI C również. Teraz trzeba by sprawdzić jeszcze kilka rzeczy:
- Czy w C++17 czegoś nie planują zmienić. Stan faktyczny jest taki, że ludzie z tego korzystają, i nie ma chyba żadnych przeciwskazań, żeby przynajmniej dla POD na omawiane zachowanie zezwolić.
- Jak reagują na to narzędzia typu typu UBSan (undefined behavior sanitizer)
- Poszperać po dokumentacji GCC, LLVM i Visual C++ czy to jakoś oficjalnie dodefiniowują (czy może kiedyś zaczną warningi na to rzucać, albo jak pojawią się w końcu pointery-dowiązane-do-arrayów, zaczną lecieć runtime exceptiony).
Anyway, @kq, rzuć plz na to okiem też - masz dobre oko do standardu, lepsze niż ja.