Dlaczego C nie zezwala na konkatenację ciągów podczas korzystania z operatora warunkowego?

95

Poniższy kod kompiluje się bez problemów:

int main() {
    printf("Hi" "Bye");
}

Jednak to się nie kompiluje:

int main() {
    int test = 0;
    printf("Hi" (test ? "Bye" : "Goodbye"));
}

Jaki jest tego powód?

José D.
źródło
95
Łączenie ciągów jest częścią wczesnej fazy leksowania; nie jest częścią wyrażenia synatx z C. Innymi słowy, nie ma wartości typu „string literal”. Zamiast tego, literały łańcuchowe są elementami leksykalnymi kodu źródłowego, które tworzą wartości.
Kerrek SB
24
Dla wyjaśnienia odpowiedź @KerrekSB - konkatenacja ciągów jest częścią wstępnego przetwarzania tekstu kodu przed jego kompilacją. Podczas gdy operator trójargumentowy jest oceniany w czasie wykonywania, po skompilowaniu kodu (lub w przypadku, gdy wszystko jest stałe, można to zrobić w czasie kompilacji).
Eugene Sh.
2
Szczegóły: W tym poście "Hi"i "Bye"są to literały ciągów, a nie łańcuchy, jak są używane w standardowej bibliotece C. W przypadku literałów łańcuchowych kompilator dokona konkatenacji "H\0i" "B\0ye". Nie to samo zsprintf(buf,"%s%s", "H\0i" "B\0ye");
chux - Przywróć Monikę
15
Mniej więcej z tego samego powodu, którego nie możesz zrobića (some_condition ? + : - ) b
user253751
4
Zauważ, że nawet printf("Hi" ("Bye"));nie zadziała - nie wymaga operatora trójskładnikowego; nawias jest wystarczający (choć printf("Hi" test ? "Bye" : "Goodbye")również nie można go skompilować). Istnieje tylko ograniczona liczba tokenów, które mogą występować po literale ciągu. Przecinek ,, otwarty nawias kwadratowy [, zamknięty nawias kwadratowy ](jak w 1["abc"]- i tak, jest makabryczny), zamykający nawias okrągły ), zamykający nawias klamrowy }(w inicjatorze lub podobnym kontekście) i średnik ;są dozwolone (i inny literał ciągu); Nie jestem pewien, czy są inne.
Jonathan Leffler

Odpowiedzi:

121

Zgodnie ze standardem C (5.1.1.2 Fazy tłumaczenia)

1 Pierwszeństwo między regułami składni tłumaczenia określają następujące fazy 6)

  1. Sąsiednie tokeny literału ciągu są konkatenowane.

I dopiero potem

  1. Białe znaki oddzielające tokeny nie mają już znaczenia. Każdy token wstępnego przetwarzania jest konwertowany na token. Powstałe tokeny są analizowane składniowo i semantycznie i tłumaczone jako jednostka translacyjna .

W tej konstrukcji

"Hi" (test ? "Bye" : "Goodbye")

nie ma sąsiadujących tokenów literału ciągu. Więc ta konstrukcja jest nieprawidłowa.

Vlad z Moskwy
źródło
43
To tylko powtarza twierdzenie, że nie jest to dozwolone w C. Nie wyjaśnia dlaczego , o co chodziło. Nie wiem, dlaczego w ciągu 5 godzin zebrał 26 głosów upvotes ... i nie mniej! Gratulacje.
Wyścigi lekkości na orbicie
4
Muszę się zgodzić z @LightnessRacesinOrbit tutaj. Dlaczego nie należy (test ? "Bye" : "Goodbye")ewaluować do żadnego z literałów łańcuchowych, które zasadniczo tworzą "Hi" "Bye" lub "Hi Goodbye"? (inne odpowiedzi na moje pytanie)
Szalony
48
@LightnessRacesinOrbit, ponieważ kiedy ludzie zwykle pytają, dlaczego coś nie kompiluje się w C, proszą o wyjaśnienie, która zasada łamie, a nie dlaczego Autorzy Standardów w starożytności tak wybrali.
user1717828
4
@LightnessRacesinOrbit Pytanie, które opisujesz, prawdopodobnie byłoby poza tematem. Nie widzę żadnego technicznego powodu, dla którego nie byłoby to możliwe, więc bez ostatecznej odpowiedzi autorów specyfikacji wszystkie odpowiedzi byłyby oparte na opiniach. I generalnie nie należałoby to do kategorii pytań „praktycznych” lub „ wymagających odpowiedzi” (jak wskazuje centrum pomocy , że potrzebujemy).
jpmc26
12
@LightnessRacesinOrbit To wyjaśnia dlaczego : „ponieważ tak mówi standard C”. Pytanie o to, dlaczego ta reguła jest zdefiniowana jako zdefiniowana, byłoby nie na temat.
user11153
135

Zgodnie ze standardem C11, rozdział §5.1.1.2, konkatenacja sąsiednich literałów ciągów:

Sąsiednie tokeny literału ciągu są konkatenowane.

dzieje się w fazie tłumaczenia . Z drugiej strony:

printf("Hi" (test ? "Bye" : "Goodbye"));

obejmuje operator warunkowy, który jest oceniany w czasie wykonywania . Tak więc w czasie kompilacji, podczas fazy tłumaczenia, nie ma żadnych sąsiednich literałów ciągów, dlatego konkatenacja nie jest możliwa. Składnia jest nieprawidłowa i dlatego została zgłoszona przez Twój kompilator.


Aby nieco rozwinąć część dlaczego , w fazie przetwarzania wstępnego sąsiednie literały łańcuchowe są łączone i reprezentowane jako pojedynczy literał ciągu (token). Magazyn jest odpowiednio przydzielany, a połączony literał ciągu jest traktowany jako pojedyncza jednostka (jeden literał ciągu).

Z drugiej strony, w przypadku konkatenacji w czasie wykonywania, miejsce docelowe powinno mieć wystarczającą ilość pamięci, aby pomieścić połączony literał ciągu, w przeciwnym razie nie będzie możliwości uzyskania dostępu do oczekiwanych połączonych danych wyjściowych. Teraz, w przypadku napisowych , są już przydzielone pamięci w czasie kompilacji i nie może być przedłużony , aby dopasować w każdym wejściem więcej przychodzących na lub dołączone do oryginalnej treści. Innymi słowy, nie będzie możliwości uzyskania dostępu do połączonego wyniku (zaprezentowania go) jako pojedynczego literału ciągu . Tak więc ta konstrukcja jest z natury niepoprawna.

Po prostu do Twojej wiadomości, do konkatenacji ciągów w czasie wykonywania ( nie literałów ) mamy funkcję biblioteczną, strcat()która łączy dwa ciągi . Uwaga, opis wspomina:

char *strcat(char * restrict s1,const char * restrict s2);

strcat()Funkcja dołącza kopię napisu wskazywany przez s2(łącznie z kończącym znakiem null) na końcu łańcucha wskazywanego przezs1 . Początkowy znak s2nadpisuje pusty znak na końcu s1. […]

Widzimy więc, że s1jest to ciąg znaków , a nie literał ciągu . Ponieważ jednak zawartość s2nie jest zmieniana w żaden sposób, może to być dosłowny ciąg znaków .

Sourav Ghosh
źródło
1
możesz chcieć dodać dodatkowe wyjaśnienie na temat strcat: tablica docelowa musi być wystarczająco długa, aby otrzymać znaki z s2plus terminator zerowy po znakach już tam obecnych.
chqrlie
39

Preprocesor przeprowadza konkatenację literału ciągów znaków w czasie kompilacji. Nie ma sposobu, aby ta konkatenacja była świadoma wartości test, która nie jest znana, dopóki program faktycznie nie zostanie wykonany. Dlatego nie można łączyć tych literałów ciągów.

Ponieważ ogólny przypadek jest taki, że nie miałbyś takiej konstrukcji dla wartości znanych w czasie kompilacji, standard C został zaprojektowany, aby ograniczyć funkcję auto-konkatenacji do najbardziej podstawowego przypadku: kiedy literały są dosłownie obok siebie .

Ale nawet gdyby nie sformułował tego ograniczenia w ten sposób lub gdyby ograniczenie zostało inaczej skonstruowane, twój przykład nadal byłby niemożliwy do zrealizowania bez przekształcenia konkatenacji w proces wykonawczy. Do tego mamy funkcje biblioteczne, takie jak strcat.

Wyścigi lekkości na orbicie
źródło
3
Po prostu przeczytałem założenia. Chociaż to, co mówisz, jest prawie poprawne, nie możesz podać źródeł tego, ponieważ ich nie ma. Jedynym źródłem w odniesieniu do C jest standardowy dokument, który (choć w wielu przypadkach jest oczywisty) nie podaje, dlaczego niektóre rzeczy są takie, jakie są, ale po prostu stwierdza, że ​​muszą być w ten specyficzny sposób. Tak więc bycie tak wybrednym w stosunku do Vlada z odpowiedzi Moskwy jest niewłaściwe. Ponieważ OP można rozbić na „Dlaczego tak jest?” -Gdy jedyną poprawną odpowiedzią źródłową jest „Ponieważ jest C, i tak definiuje się C”, jest to jedyna dosłownie prosta poprawna odpowiedź.
dhein
1
Jest to (przyznane) bez wyjaśnienia. Ale tutaj znowu powiedziano, że odpowiedź Vlada służy znacznie bardziej jako wyjaśnienie podstawowego problemu, niż twoja. Znowu powiedział: Chociaż informacje, które podasz, które mogę potwierdzić, są powiązane i poprawne, nie zgadzam się z twoimi skargami. i chociaż nie uważałbym was również za offtopic, to z mojego POV jest bardziej offtopic niż w rzeczywistości Vlads.
dhein
11
@Zaibis: Źródłem jestem ja. Odpowiedź Vlada wcale nie jest wyjaśnieniem; jest jedynie potwierdzeniem przesłanki pytania. Z pewnością żaden z nich nie jest „poza tematem” (warto sprawdzić, co oznacza ten termin). Ale masz prawo do swojej opinii.
Wyścigi lekkości na orbicie
Nawet po przeczytaniu powyższych komentarzy nadal zastanawiam się, kto zagłosował w dół na tę odpowiedź ᶘ ᵒᴥᵒᶅ Uważam, że jest to idealna odpowiedź, chyba że OP poprosi o dalsze wyjaśnienia dotyczące tej odpowiedzi.
Mohit Jain
2
Nie jestem w stanie określić, dlaczego ta odpowiedź jest dla Ciebie do przyjęcia, a @ VladfromMoscow nie, skoro obaj mówią to samo i kiedy jego jest poparta cytatem, a twoja nie.
Markiz Lorne
30

Ponieważ C nie ma stringtypu. Literały ciągów są kompilowane do chartablic, do których odwołuje się char*wskaźnik.

C umożliwia łączenie sąsiednich literałów w czasie kompilacji , jak w pierwszym przykładzie. Sam kompilator C ma pewną wiedzę na temat łańcuchów. Ale ta informacja nie jest obecna w czasie wykonywania, a zatem nie może nastąpić konkatenacja.

Podczas procesu kompilacji Twój pierwszy przykład jest „tłumaczony” na:

int main() {
    static const char char_ptr_1[] = {'H', 'i', 'B', 'y', 'e', '\0'};
    printf(char_ptr_1);
}

Zwróć uwagę, jak dwa ciągi są łączone w pojedynczą tablicę statyczną przez kompilator, zanim program kiedykolwiek zostanie wykonany.

Jednak Twój drugi przykład jest „przetłumaczony” na coś takiego:

int main() {
    static const char char_ptr_1[] = {'H', 'i', '\0'};
    static const char char_ptr_2[] = {'B', 'y', 'e', '\0'};
    static const char char_ptr_3[] = {'G', 'o', 'o', 'd', 'b', 'y', 'e', '\0'};
    int test = 0;
    printf(char_ptr_1 (test ? char_ptr_2 : char_ptr_3));
}

Powinno być jasne, dlaczego to się nie kompiluje. Operator trójskładnikowy ?jest oceniany w czasie wykonywania, a nie w czasie kompilacji, kiedy „łańcuchy” nie istnieją już jako takie, ale tylko jako proste chartablice, do których odwołują się char*wskaźniki. W przeciwieństwie do sąsiednich literałów łańcuchowych , sąsiednie wskaźniki znaków są po prostu błędem składniowym.

Bez podpisu
źródło
2
Doskonała odpowiedź, prawdopodobnie najlepsza tutaj. „Powinno być jasne, dlaczego to się nie kompiluje”. Można rozważyć rozszerzenie tego wyrażeniem „ponieważ operator trójargumentowy jest wartością warunkową ocenianą w czasie wykonywania, a nie w czasie kompilacji ”.
kot
Nie powinno static const char *char_ptr_1 = {'H', 'i', 'B', 'y', 'e', '\0'};być static const char *char_ptr_1 = "HiBye";i podobnie w przypadku pozostałych wskazówek?
Spikatrix
@CoolGuy Kiedy piszesz, static const char *char_ptr_1 = "HiBye";kompilator tłumaczy wiersz na static const char *char_ptr_1 = {'H', 'i', 'B', 'y', 'e', '\0'};, więc nie, nie powinien być zapisywany „jak napis”. Jak mówi Odpowiedź, łańcuchy są kompilowane do tablicy znaków, a jeśli przypisujesz tablicę znaków w jej najbardziej „surowej” formie, użyłbyś listy znaków oddzielonych przecinkami, tak jakstatic const char *char_ptr_1 = {'H', 'i', 'B', 'y', 'e', '\0'};
Ankush
3
@Ankush Yes. Ale chociaż static const char str[] = {'t', 'e', 's', 't', '\0'};to to samo co static const char str[] = "test";, static const char* ptr = "test";to nie to samo co static const char* ptr = {'t', 'e', 's', 't', '\0'};. Pierwsza jest ważna i będzie się kompilować, ale druga jest nieprawidłowa i robi to, czego oczekujesz.
Spikatrix
Uzupełniłem ostatni akapit i poprawiłem przykłady kodu, dzięki!
Niepodpisany
13

Jeśli naprawdę chcesz, aby obie gałęzie tworzyły stałe łańcuchowe czasu kompilacji, które będą wybierane w czasie wykonywania, potrzebujesz makra.

#include <stdio.h>
#define ccat(s, t, a, b) ((t)?(s a):(s b))

int
main ( int argc, char **argv){
  printf("%s\n", ccat("hello ", argc > 2 , "y'all", "you"));
  return 0;
}
Eric
źródło
10

Jaki jest tego powód?

Twój kod używający operatora trójargumentowego warunkowo wybiera między dwoma literałami ciągów. Bez względu na znany lub nieznany stan, nie można tego ocenić w czasie kompilacji, więc nie można go skompilować. Nawet to stwierdzenie printf("Hi" (1 ? "Bye" : "Goodbye"));się nie skompiluje. Powód został szczegółowo wyjaśniony w powyższych odpowiedziach. Inna możliwość uczynienia takiej instrukcji przy użyciu operatora trójskładnikowego ważnego do kompilacji , wymagałaby również zastosowania znacznika formatu i wyniku potrójnej instrukcji operatora sformatowanej jako dodatkowy argument do printf. Nawet wtedy printf()wydruk sprawiałby wrażenie „konkatenacji” tych ciągów tylko w czasie wykonywania i już w czasie wykonywania .

#include <stdio.h>

int main() {
    int test = 0;
    printf("Hi %s\n", (test ? "Bye" : "Goodbye")); //specify format and print as result
}
user3078414
źródło
3
SO nie jest witryną z samouczkami. Powinieneś udzielić odpowiedzi na OP, a nie samouczek.
Michi
1
To nie odpowiada na pytanie PO. Może to być próba rozwiązania podstawowego problemu PO, ale tak naprawdę nie wiemy, co to jest.
Keith Thompson
1
printfnie wymaga specyfikatora formatu; gdyby tylko konkatenacja została wykonana w czasie kompilacji (a tak nie jest), użycie printf przez OP byłoby poprawne.
David Conrad
Dziękuję za twoją uwagę, @David Conrad. Moje niedbałe sformułowanie rzeczywiście sprawiłoby, że stwierdzenie printf()wymagałoby znacznika formatu, co jest absolutnie nieprawdą. Poprawione!
user3078414
To lepsze sformułowanie. +1 dzięki.
David Conrad
7

W printf("Hi" "Bye");masz dwie kolejne tablice znaków, które kompilator może przekształcić w jedną tablicę.

W printf("Hi" (test ? "Bye" : "Goodbye"));masz jedną tablicę, po której następuje wskaźnik do znaku (tablica przekonwertowana na wskaźnik do jej pierwszego elementu). Kompilator nie może scalić tablicy i wskaźnika.

pmg
źródło
0

Aby odpowiedzieć na pytanie - przejdę do definicji printf. Funkcja printf oczekuje const char * jako argumentu. Dowolny literał łańcuchowy, taki jak „Hi”, jest stałym char *; jednakże wyrażenie takie jak (test)? "str1" : "str2"NIE jest const char *, ponieważ wynik takiego wyrażenia znajduje się tylko w czasie wykonywania, a zatem jest nieokreślony w czasie kompilacji, co właściwie powoduje, że kompilator narzeka. Z drugiej strony - działa to doskonaleprintf("hi %s", test? "yes":"no")

Stats_Lover
źródło
* jednakże wyrażenie takie jak (test)? "str1" : "str2"NIE jest const char*... Oczywiście, że tak! Nie jest to wyrażenie stałe, ale jego typ jest const char * . Byłoby doskonale pisać printf(test ? "hi " "yes" : "hi " "no"). Problem OP nie ma nic wspólnego printf, "Hi" (test ? "Bye" : "Goodbye")jest błędem składniowym bez względu na kontekst wyrażenia.
chqrlie
Zgoda.
Pomyliłem
-4

To się nie kompiluje, ponieważ lista parametrów funkcji printf to

(const char *format, ...)

i

("Hi" (test ? "Bye" : "Goodbye"))

nie pasuje do listy parametrów.

gcc próbuje to zrozumieć, wyobrażając sobie to

(test ? "Bye" : "Goodbye")

jest listą parametrów i narzeka, że ​​„Hi” nie jest funkcją.

Rodbots
źródło
6
Witamy w Stack Overflow. Masz rację, że nie pasuje do printf()listy argumentów, ale to dlatego, że wyrażenie nie jest prawidłowe nigdzie - nie tylko na printf()liście argumentów. Innymi słowy, wybrałeś zbyt wyspecjalizowany powód problemu; ogólny problem polega na tym, że "Hi" (nie jest to poprawne w C, nie mówiąc już o wywołaniu do printf(). Proponuję usunąć tę odpowiedź, zanim zostanie poddana głosowaniu w dół.
Jonathan Leffler
Nie tak działa C. Nie jest to analizowane jako próba wywołania literału ciągu, takiego jak PHP.
kot