Łącząc C ++ i C - jak działa #ifdef __cplusplus?

319

Pracuję nad projektem, który ma dużo starszego kodu C. Zaczęliśmy pisać w C ++ z zamiarem ostatecznej konwersji starszego kodu. Jestem trochę zdezorientowany co do interakcji między C i C ++. Rozumiem, że poprzez owinięcie C kodu z extern "C"C ++ kompilator nie będzie Magiel C pseudonimów, ale nie jestem do końca pewien, jak zaimplementować to.

Tak więc u góry każdego pliku nagłówka C (po strażnikach dołączania) mamy

#ifdef __cplusplus
extern "C" {
#endif

a na dole piszemy

#ifdef __cplusplus
}
#endif

Pomiędzy nimi mamy wszystkie prototypy dołączania, typedef i funkcji. Mam kilka pytań, aby sprawdzić, czy rozumiem to poprawnie:

  1. Jeśli mam plik C ++ A.hh, który zawiera plik nagłówka C Bh, zawiera inny plik C nagłówka Ch, jak to działa? Myślę, że kiedy kompilator wejdzie w Bh, __cpluspluszostanie zdefiniowany, więc otoczy kod extern "C" (i __cplusplusnie zostanie zdefiniowany w tym bloku). Tak więc, gdy wejdzie w Ch, __cplusplusnie zostanie zdefiniowany, a kod nie zostanie zawinięty extern "C". Czy to jest poprawne?

  2. Czy jest coś złego w pakowaniu kodu extern "C" { extern "C" { .. } }? Co zrobi drugi extern "C" ?

  3. Nie umieszczamy tego opakowania wokół plików .c, tylko pliki .h. Co się stanie, jeśli funkcja nie będzie miała prototypu? Czy kompilator uważa, że ​​jest to funkcja C ++?

  4. Używamy również kodu innej firmy, który jest napisany w C i nie ma tego rodzaju opakowania. Za każdym razem, gdy dołączam nagłówek z tej biblioteki, umieszczam extern "C"wokół #include. Czy to właściwy sposób, aby sobie z tym poradzić?

  5. Wreszcie, czy to dobry pomysł? Czy jest coś jeszcze, co powinniśmy zrobić? Będziemy mieszać C i C ++ w dającej się przewidzieć przyszłości i chcę się upewnić, że zajmiemy się wszystkimi naszymi bazami.

Dublev
źródło
2
W skrócie, to najlepsze wytłumaczenie: To ensure that the names declared in that portion of code have C linkage, and thus C++ name mangling is not performed. (mam to z linku )
anhldbk
Nie musisz pogrubiać nazwy języka C
Edward Karak

Odpowiedzi:

290

extern "C"tak naprawdę nie zmienia sposobu, w jaki kompilator odczytuje kod. Jeśli twój kod znajduje się w pliku .c, zostanie skompilowany jako C, jeśli znajduje się w pliku .cpp, zostanie skompilowany jako C ++ (chyba że zrobisz coś dziwnego w swojej konfiguracji).

To, extern "C"co wpływa na połączenie. Po kompilacji funkcje C ++ mają zmienione nazwy - właśnie dlatego możliwe jest przeładowanie. Nazwa funkcji jest modyfikowana na podstawie typów i liczby parametrów, dzięki czemu dwie funkcje o tej samej nazwie będą miały różne nazwy symboli.

Kod wewnątrz extern "C"jest nadal kodem C ++. Istnieją ograniczenia dotyczące tego, co można zrobić w zewnętrznym bloku „C”, ale wszystkie dotyczą łączenia. Nie można zdefiniować żadnych nowych symboli, których nie można zbudować za pomocą połączenia C. Oznacza to na przykład brak klas lub szablonów.

extern "C"bloki ładnie się zagnieżdżają. Jest też, extern "C++"jeśli znajdziesz się beznadziejnie uwięziony w extern "C"regionach, ale nie jest to dobry pomysł z punktu widzenia czystości.

Teraz, szczególnie w odniesieniu do ponumerowanych pytań:

Odnośnie # 1: __cplusplus pozostanie zdefiniowany wewnątrz extern "C"bloków. Nie ma to jednak znaczenia, ponieważ bloki powinny być starannie zagnieżdżone.

Odnośnie # 2: __cplusplus zostanie zdefiniowany dla każdej jednostki kompilacji uruchomionej przez kompilator C ++. Ogólnie oznacza to, że pliki .cpp i wszelkie pliki zawarte w tym pliku .cpp. To samo .h (lub .hh lub .hpp lub what-have-you) może być interpretowane jako C lub C ++ w różnych momentach, jeśli zawierają je różne jednostki kompilacji. Jeśli chcesz, aby prototypy w pliku .h odwoływały się do nazw symboli C, muszą one mieć, extern "C"gdy są interpretowane jako C ++, a nie powinny, extern "C"gdy są interpretowane jako C - stąd #ifdef __cplusplussprawdzanie.

Aby odpowiedzieć na pytanie nr 3: funkcje bez prototypów będą miały powiązanie z C ++, jeśli znajdują się w plikach .cpp, a nie w extern "C"bloku. Jest to w porządku, ponieważ jeśli nie ma prototypu, może być wywoływane tylko przez inne funkcje w tym samym pliku, a wtedy ogólnie nie obchodzi Cię, jak wygląda połączenie, ponieważ nie planujesz mieć tej funkcji i tak może zostać wywołany przez cokolwiek poza tą samą jednostką kompilacji.

W przypadku nr 4 masz to dokładnie. Jeśli dołączasz nagłówek kodu z łączeniem C (np. Kod skompilowany przez kompilator C), musisz extern "C"nagłówek - w ten sposób będziesz mógł połączyć się z biblioteką. (W przeciwnym razie twój linker szukałby funkcji o nazwach takich jak _Z1hicwtedy, gdy szukałeśvoid h(int, char)

5: Tego rodzaju miksowanie jest częstym powodem do używania extern "C"i nie widzę nic złego w robieniu tego w ten sposób - po prostu upewnij się, że rozumiesz, co robisz.

Andrew Shelansky
źródło
10
extern "C++"
Warto
1
Napisałem prosty program C. Wewnątrz dodałem blok #ifdef __cplusplus i dodałem printf („__ cplusplus zdefiniowany \ n”); w tym. Jeśli skompiluję go za pomocą gcc, „__cplusplus zdefiniowany” nie zostanie wydrukowany, ale jeśli skompiluję go za pomocą g ++, zostanie wydrukowany. Uważam więc, że __cplusplus oznacza, że ​​kompilatorem jest kompilator C ++ (tak powiedziałeś). Czy to nie jest poprawne? (ponieważ widziałem, jak mówisz, że „__cplusplus powinien być zdefiniowany wewnątrz zewnętrznych bloków„ C ”. Czy możemy zdefiniować __cplusplus wyraźnie?
Chan Kim
1
Chociaż powinieneś być w stanie zdefiniować (prawie) wszystko, co chcesz, chodzi o __cplusplusto, aby określić, czy C++jest używany, czy też C, więc zdefiniowanie go ręcznie / wyraźnie przeciwstawia się jego celowi ...
nurchi
Zewnętrzne „C” w rzeczywistości nie dotyczy tego, jak kompilator wyświetla plik źródłowy, chodzi tylko o to, jak przegląda plik nagłówkowy. Struktury mogą mieć różne rozmiary, gdy są kompilowane jako C vs. C ++, istnieje oczywiście mangling nazwy i potencjalnie inne różnice.
Nick
39
  1. extern "C"nie zmienia obecności ani nieobecności __cplusplusmakra. To po prostu zmienia powiązanie i zniekształcanie nazw zapakowanych deklaracji.

  2. Możesz zagnieżdżać extern "C"bloki całkiem szczęśliwie.

  3. Jeśli skompilujesz swoje .cpliki jako C ++, wszystko, co nie jest w extern "C"bloku i bez extern "C"prototypu, będzie traktowane jako funkcja C ++. Jeśli skompilujesz je jako C, wtedy oczywiście wszystko będzie funkcją C.

  4. tak

  5. W ten sposób możesz bezpiecznie mieszać C i C ++.

Anthony Williams
źródło
Jeśli kompilujesz .cpliki jako C ++, wszystko jest kompilowane jako kod C ++, nawet jeśli jest w extern "C"bloku. extern "C"Kod nie może korzystać z funkcji zależnych od C ++ konwencji wywołań (np przeciążenia sieci operatora), lecz z korpusem funkcją jest sporządzonych jak C ++, ze wszystkim, co za sobą pociąga.
David C.
21

Kilka błędów, które są obietnicami doskonałej odpowiedzi Andrew Shelansky'ego i trochę się z tym nie zgadzać, tak naprawdę nie zmienia sposobu, w jaki kompilator odczytuje kod

Ponieważ prototypy funkcji są kompilowane jako C, nie można przeciążać tych samych nazw funkcji różnymi parametrami - jest to jedna z kluczowych cech mieszania nazw kompilatora. Jest to opisywane jako problem z połączeniem, ale nie jest to do końca prawdą - będziesz otrzymywać błędy zarówno od kompilatora, jak i linkera.

Błędy kompilatora wystąpią, jeśli spróbujesz użyć funkcji C ++ deklaracji prototypu, takich jak przeciążenie.

Błędy linkera pojawią się później, ponieważ wydaje się, że Twoja funkcja nie została znaleziona, jeśli nie masz zewnętrznego „C” opakowania wokół deklaracji, a nagłówek jest zawarty w mieszaninie źródeł C i C ++.

Jednym z powodów, aby zniechęcać ludzi do używania kompilacji C jako ustawienia C ++ jest to, że oznacza to, że ich kod źródłowy nie jest już przenośny. To ustawienie jest ustawieniem projektu, więc jeśli plik .c zostanie upuszczony do innego projektu, nie zostanie skompilowany jako c ++. Wolałbym, żeby ludzie poświęcili czas na zmianę nazwy sufiksów plików na .cpp.

Andy Dent
źródło
1
To była tajemnicza przyczyna, wyciągając moje włosy. Naprawdę trzeba gdzieś opublikować.
Mitchell Currie,
3

Chodzi o ABI, aby zarówno aplikacja C, jak i C ++ mogły bez problemu korzystać z interfejsów C.

Ponieważ język C jest bardzo łatwy, generowanie kodu było stabilne przez wiele lat dla różnych kompilatorów, takich jak GCC, Borland C \ C ++, MSVC itp.

Podczas gdy C ++ staje się coraz bardziej popularny, do nowej domeny C ++ należy dodać wiele rzeczy (na przykład ostatecznie Cfront został porzucony w AT&T, ponieważ C nie mógł obejmować wszystkich potrzebnych funkcji). Tak jak w przeszłości funkcja szablonu i generowanie kodu czasu kompilacji, różni dostawcy różnych kompilatorów faktycznie wykonywali osobną implementację kompilatora i linkera C ++ osobno, rzeczywiste ABI nie są w ogóle kompatybilne z programem C ++ na różnych platformach.

Ludzie mogą nadal chcieć zaimplementować rzeczywisty program w C ++, ale nadal zachowują stary interfejs C i ABI jak zwykle, plik nagłówkowy musi zadeklarować zewnętrzny „C” {} , informuje kompilator, aby wygenerował zgodny / stary / prosty / łatwy C ABI dla funkcji interfejsu, jeśli kompilatorem jest kompilator C, a nie kompilator C ++.

Bo Zhou
źródło