2013-04-28 14 views
8

Niedawno dowiedziałem się, że the spiral rule odtajania złożonych deklaracji, które musiały być napisane z serii typedefs. Jednak następujący komentarz alarmy mnie:Spiralna zasada dotycząca deklaracji - kiedy jest to błąd?

A frequently cited simplification, which only works for a few simple cases.

nie znajdę void (*signal(int, void (*fp)(int)))(int); „prosty przypadek”. A przy okazji, co jest tym bardziej niepokojące.

Moje pytanie brzmi: w których sytuacjach będę poprawny w stosowaniu reguły, w której wystąpiłby błąd?

+3

Proponuję zapytać 'Vorac' kiedy to pójdzie źle; Nie jestem świadomy okoliczności, w których coś pójdzie nie tak. –

+0

@ Jonathan Leffler, więc nie ma żadnych znanych problemów i spiralna zasada nigdy nie jest błędna? – Vorac

+1

OK - to [James Kanze] (http://stackoverflow.com/users/649665/james-kanze) ma [widoki] (http://stackoverflow.com/questions/7526152/easy-rule-to -read-complicated-const-declarationations/7526298 # comment23257858_7526298) na temat artykułu opublikowanego w 1994 roku (nie ty, przepraszam za błędy w interpretacji). Nie wiem, jaki jest sprzeciw Jamesa; Nie wiem o okolicznościach, w których zawodzi (ale nie studiowałem tego zbyt mocno). To nie jest to samo, co powiedzenie, że nie ma takich okoliczności, ale uznałbym ten niewyjaśniony, niepotwierdzony komentarz za tak wiele dymu. –

Odpowiedz

2

Np .:

int * a[][5]; 

nie jest tablicą wskaźników do tablic int.

+0

Więc co to jest? – yapkm01

+0

@ yapkm01 Jest to tablica tablic z 5 wskaźnikami do 'int's. –

3

Reguła jest poprawna. Należy jednak bardzo ostrożnie go stosować.

Proponuję zastosować go bardziej formalnie do deklaracji C99 +.

Najważniejszą rzeczą jest to, aby rozpoznać następujące rekurencyjnej struktury wszystkich deklaracji (const, volatile, static, extern, inline, struct, union, typedef są usuwane z rysunku dla uproszczenia, ale można dodać z powrotem łatwo):

base-type [derived-part1: *'s] [object] [derived-part2: []'s or()] 

Tak, to wszystko, cztery części.

where 

    base-type is one of the following (I'm using a bit compressed notation): 
    void 
    [signed/unsigned] char 
    [signed/unsigned] short [int] 
    signed/unsigned [int] 
    [signed/unsigned] long [long] [int] 
    float 
    [long] double 
    etc 

    object is 
     an identifier 
    OR 
     ([derived-part1: *'s] [object] [derived-part2: []'s or()]) 

    * is *, denotes a reference/pointer and can be repeated 
    [] in derived-part2 denotes bracketed array dimensions and can be repeated 
() in derived-part2 denotes parenthesized function parameters delimited with ,'s 
    [] elsewhere denotes an optional part 
() elsewhere denotes parentheses 

Gdy masz wszystkie 4 części analizowany,

    [object] jest [derived-part2 (zawierający/powrót)] [derived-part2 (wskaźnik)] base-type .

Jeśli istnieje rekursja, znajdziesz numer object (jeśli jest jakiś) na dole stosu rekursji, będzie to najbardziej wewnętrzna i dostaniesz pełną deklarację, przechodząc do góry i zbierając i łącząc wyprowadzone części na każdym poziomie rekurencji.

Podczas analizowania można przenieść [object] po [derived-part2] (jeśli istnieje). To da ci zlinearyzowaną, łatwą do zrozumienia deklarację (zob. Powyżej:).

Zatem w

char* (**(*foo[3][5])(void))[7][9]; 

otrzymasz:

  1. base-type = char
  2. Poziom 1: derived-part1 = *, object = (**(*foo[3][5])(void)), derived-part2 = [7][9]
  3. poziom 2: derived-part1 = **, object = (*foo[3][5]), derived-part2 = (void)
  4. poziom 3: derived-part1 = *, object = foo, derived-part2 = [3][5]

Stamtąd:

  1. Poziom 3 *[3][5]foo
  2. poziom 2: **(void)*[3][5]foo
  3. poziom 1: *[7][9]**(void)*[3][5]foo
  4. wreszcie char*[7][9]**(void)*[3][5]foo

Teraz, czytając od prawej do lewej:

foo to tablica złożona z 3 tablic składających się z 5 wskaźników do funkcji (nie pobierającej żadnych parametrów) zwracająca wskaźnik do wskaźnika 7 tablic z 9 wskaźników do znaku.

Można również odwrócić wymiary matrycy w każdym procesie derived-part2.

To twoja spiralna zasada.

I łatwo jest zobaczyć spiralę. Zanurz się w coraz głębiej zagnieżdżone od lewej, a następnie powróć z prawej strony, aby zauważyć, że na wyższym poziomie znajduje się kolejna para lewej i prawej strony i tak dalej.

+0

Interesujące. Najpierw mówisz, że reguła jest prawidłowa, a następnie dajesz wyjaśnienie, jak odczytać deklarację, która jej nie używa, aw rzeczywistości jej przeczy. –

+0

Możesz wspomnieć, że kiedy się powtarzasz (używałbym terminu nest, ale sprowadza się to do tego samego), zagnieżdżone części deklaracji nie zawierają twojej 'części bazowej' (z wyjątkiem parametrów funkcji, gdzie mieć pełną deklarację zagnieżdżoną w innej deklaracji). –

+0

@JamesKanze To, o czym chcesz wspomnieć, powinno być natychmiast widoczne. Zauważ, że nie ma żadnego "typu bazowego" w dowolnym miejscu pod 'object is ...'. –

16

Zasadniczo rzecz biorąc, zasada po prostu nie działa, bo inaczej prace redefinicji tego, co rozumie się przez spiralę (w tym przypadku, nie ma sensu się nad tym zastanowić, np.

int* a[10][15]; 

Reguła spiralna daje a jest tablicą [10] wskaźnika do array [15] int, co jest błędne.Jeżeli twoja strona, to też nie działa, w rzeczywistości w przypadku signal , to nie jest jasne, gdzie powinieneś zacząć spiralę.

Zasadniczo łatwiej jest znaleźć przykłady, w których reguła zawodzi niż w przykładach, w których działa.

Często mam pokusę, aby powiedzieć, że parsowanie deklaracji C++ jest proste, ale żadna z osób, która próbowała skomplikowanych deklaracji, nie uwierzyłaby mi w to, . Z drugiej strony nie jest tak trudne, jak to się czasem wydaje. Sekretem jest myślenie o deklaracji dokładnie tak samo jak wyrażenie, ale z dużo mniejszą liczbą operatorów i bardzo prostą zasadą pierwszeństwa: wszyscy operatorzy po prawej stronie mają pierwszeństwo przed wszystkimi operatorami po lewej. W przypadku braku nawiasów oznaczenie powoduje przetworzenie wszystkiego najpierw na prawo, a następnie wszystko na lewo i przetwarzanie nawiasów dokładnie tak samo, jak w każdym innym wyrażeniu. rzeczywista trudność polega nie składnia per se, ale że wyników jest kilka bardzo skomplikowane i intuicyjne deklaracje, w szczególności tam, gdzie zaangażowane są zwracane wartości funkcyjne i wskaźniki do funkcje: pierwsza w prawo, to znaczy wtedy lewy reguła że operatorzy na określonym poziomie często powszechnie oddziela np

int (*f(/* lots of parameters */))[10]; 

końcowe określenie w rozwoju tutaj jest int[10], ale wprowadzenie z [10] po całkowitym opisie funkcyjnego (w najmniej dla mnie) bardzo nienaturalne, i muszę przestać i wypracować to za każdym razem . (Prawdopodobnie ta tendencja do logicznie sąsiadujących części rozprzestrzenia się, co prowadzi do spiralnej reguły.Od problem jest oczywiście, że w przypadku braku nawiasów, nie zawsze są one zawsze rozłożone — za każdym razem, gdy widzisz [i][j], zasada jest iść prawo, a następnie przejść jeszcze raz w prawo, zamiast spirali)

A ponieważ jesteśmy teraz myśli o deklaracji w zakresie wyrażeń:. co zrobić, gdy wyrazem staje się zbyt skomplikowane czytać? Wprowadzasz zmienne pośrednie w kolejności , aby ułatwić czytanie. W przypadku deklaracji, "zmienne pośrednie" to typedef. W szczególności chciałbym, aby twierdził, że za każdym razem, gdy część zwracanego typu kończy się po argumentach funkcji (i wiele innych razy), użytkownik powinien użyć , aby uczynić deklarację prostszą. (Ta jest jednak regułą "rób, co mówię, nie jak ja", boję się, że Od czasu do czasu używam bardzo złożonych deklaracji.)

+0

"W przypadku braku nawiasów oznacza to przetwarzanie wszystkiego najpierw w lewo, potem wszystko w prawo". Czy nie powinno być na odwrót? Jak sam powiedziałeś, wszyscy operatorzy po prawej mają pierwszeństwo przed wszystkimi operatorami po lewej. Powinno być: najpierw - na prawo, potem - na lewo. – AnT

+0

@AnT Tak. Zdecydowanie pomyliłem to stwierdzenie. Poprawię to, dzięki. (I jest to zaskakujące, że nikt nie zauważył błędu aż do teraz.) –