Czy łączenie obiektów C ++ 17, C ++ 14 i C ++ 11 jest bezpieczne


101

Załóżmy, że mam trzy skompilowane obiekty, wszystkie wyprodukowane przez ten sam kompilator / wersję :

  1. A został skompilowany w standardzie C ++ 11
  2. B został skompilowany w standardzie C ++ 14
  3. C został skompilowany w standardzie C ++ 17

Dla uproszczenia załóżmy, że wszystkie nagłówki zostały napisane w C ++ 11, używając tylko konstrukcji, których semantyka nie zmieniła się między wszystkimi trzema wersjami standardowymi , a więc wszelkie współzależności zostały poprawnie wyrażone przez włączenie nagłówka, a kompilator nie zgłosił obiektu.

Która kombinacja tych obiektów to jest i czy nie jest bezpieczne połączenie w pojedynczy plik binarny? Czemu?


EDYCJA: mile widziane są odpowiedzi dotyczące głównych kompilatorów (np. Gcc, clang, vs ++)


6
To nie jest pytanie do szkoły / rozmowy kwalifikacyjnej. Pytanie wynika z konkretnego przypadku: pracuję nad projektem, który zależy od biblioteki open source. Buduję tę bibliotekę ze źródła, ale jej system kompilacji akceptuje tylko flagę do wyboru między budowaniem C ++ 03 / C ++ 11. Kompilator, którego używam, obsługuje jednak inne standardy i rozważam aktualizację własnego projektu do C ++ 17. Nie jestem pewien, czy to bezpieczna decyzja. Czy może wystąpić przerwa w ABI lub inny sposób, w którym podejście nie jest zalecane? Nie znalazłem jednoznacznej odpowiedzi i postanowiłem zadać pytanie dotyczące sprawy ogólnej.
— ricab

6
Zależy to całkowicie od kompilatora. W formalnych specyfikacjach C ++ nie ma nic, co regulowałoby tę sytuację. Istnieje również niewielka możliwość, że kod napisany zgodnie ze standardami C ++ 03 lub C + 11 będzie miał pewne problemy na poziomie C ++ 14 i C ++ 17. Mając wystarczającą wiedzę i doświadczenie (i dobrze napisany kod na początek), powinno być możliwe rozwiązanie każdego z tych problemów. Jeśli jednak nie jesteś dobrze zaznajomiony z nowszymi standardami C ++, lepiej trzymaj się tego, co obsługuje system kompilacji i przetestuj pracę.
— Sam Varshavchik

10
@Someprogrammerdude: To niezwykle wartościowe pytanie. Żałuję, że nie mam odpowiedzi. Wiem tylko, że libstdc ++ przez RHEL devtoolset jest wstecznie kompatybilny z założenia, poprzez statyczne łączenie nowszych rzeczy i pozostawianie starszych rzeczy do dynamicznego rozwiązywania w czasie wykonywania przy użyciu "natywnej" biblioteki libstdc ++. Ale to nie odpowiada na pytanie.
— Wyścigi lekkości na orbicie

4
@nm: ... co jest w większości przypadków ... prawie każdy, kto rozprowadza biblioteki C ++ niezależne od dystrybucji, robi to (1) w formie biblioteki dynamicznej i (2) bez kontenerów bibliotek standardowych C ++ na granicach interfejsu. Biblioteki pochodzące z dystrybucji Linuksa są łatwe, ponieważ wszystkie są zbudowane przy użyciu tego samego kompilatora, tej samej standardowej biblioteki i prawie tego samego domyślnego zestawu flag.
— Matteo Italia

3
Tylko wyjaśnienie wcześniejszego komentarza z @MatteoItalia "i przy przełączaniu z trybu C ++ 03 na C ++ 11 (w szczególności std :: string)." To nieprawda, aktywna std::stringimplementacja w libstdc ++ jest niezależna od -stdużywanego trybu . Jest to ważna właściwość, właśnie po to, by wspierać sytuacje takie jak PO. Możesz użyć nowego std::stringkodu w C ++ 03 i możesz użyć starego std::stringw kodzie C ++ 11 (zobacz link w późniejszym komentarzu Matteo).
— Jonathan Wakely,

Odpowiedzi:


121

Która kombinacja tych obiektów to jest i czy nie jest bezpieczne połączenie w pojedynczy plik binarny? Czemu?

W przypadku GCC można bezpiecznie łączyć ze sobą dowolną kombinację obiektów A, B i C. Jeśli wszystkie są zbudowane w tej samej wersji, to są kompatybilne z ABI, wersja standardowa (tj. -stdOpcja) nie robi żadnej różnicy.

Czemu? Ponieważ jest to ważna właściwość naszej realizacji, nad którą ciężko pracujemy.

Problem występuje, gdy łączysz ze sobą obiekty skompilowane z różnymi wersjami GCC i używałeś niestabilnych funkcji nowego standardu C ++, zanim obsługa GCC dla tego standardu została zakończona. Na przykład, jeśli kompilujesz obiekt przy użyciu GCC 4.9 -std=c++11i innego obiektu z GCC 5 -std=c++11, będziesz mieć problemy. Obsługa C ++ 11 była eksperymentalna w GCC 4.x, więc wystąpiły niezgodne zmiany pomiędzy wersjami GCC 4.9 i 5 funkcji C ++ 11. Podobnie, jeśli skompilujesz jeden obiekt z GCC 7 i -std=c++17inny obiekt z GCC 8 i -std=c++17będziesz miał problemy, ponieważ obsługa C ++ 17 w GCC 7 i 8 jest wciąż eksperymentalna i ewoluuje.

Z drugiej strony będzie działać dowolna kombinacja następujących obiektów (chociaż zobacz uwagę poniżej dotyczącą libstdc++.sowersji):

  • obiekt D skompilowany za pomocą GCC 4.9 i -std=c++03
  • obiekt E skompilowany za pomocą GCC 5 i -std=c++11
  • obiekt F skompilowany za pomocą GCC 7 i -std=c++17

Dzieje się tak, ponieważ obsługa C ++ 03 jest stabilna we wszystkich trzech używanych wersjach kompilatora, a więc komponenty C ++ 03 są kompatybilne między wszystkimi obiektami. Obsługa C ++ 11 jest stabilna od GCC 5, ale obiekt D nie używa żadnych funkcji C ++ 11, a obiekty E i F używają wersji, w których obsługa C ++ 11 jest stabilna. Obsługa C ++ 17 nie jest stabilna w żadnej z używanych wersji kompilatora, ale tylko obiekt F używa funkcji C ++ 17, więc nie ma problemu ze zgodnością z pozostałymi dwoma obiektami (jedyne funkcje, które współużytkują, pochodzą z C ++ 03 lub C ++ 11, a użyte wersje sprawiają, że te części są OK). Jeśli później chciałbyś skompilować czwarty obiekt, G, używając GCC 8, a -std=c++17następnie musiałbyś przekompilować F z tą samą wersją (lub bez połączenia z F), ponieważ symbole C ++ 17 w F i G są niekompatybilne.

Jedynym zastrzeżeniem zgodności opisanej powyżej między D, E i F jest to, że twój program musi używać libstdc++.sowspólnej biblioteki z GCC 7 (lub nowszej). Ponieważ obiekt F został skompilowany z GCC 7, musisz użyć współdzielonej biblioteki z tego wydania, ponieważ kompilacja dowolnej części programu z GCC 7 może wprowadzić zależności od symboli, których nie ma w libstdc++.soGCC 4.9 lub GCC 5. Podobnie, jeśli połączyłeś się z obiektem G, zbudowanym z GCC 8, musisz użyć libstdc++.soGCC 8, aby upewnić się, że wszystkie symbole potrzebne G zostały znalezione. Prostą zasadą jest zapewnienie, że biblioteka współdzielona, ​​której program używa w czasie wykonywania, jest co najmniej tak nowa, jak wersja użyta do kompilacji dowolnego z obiektów.

Kolejnym zastrzeżeniem przy używaniu GCC, o którym wspomniano już w komentarzach do twojego pytania, jest to, że od GCC 5 istnieją dwie implementacjestd::string dostępne w libstdc ++. Te dwie implementacje nie są kompatybilne z linkami (mają różne zniekształcone nazwy, więc nie można ich łączyć), ale mogą współistnieć w tym samym pliku binarnym (mają różne zniekształcone nazwy, więc nie powodują konfliktów, jeśli jeden obiekt używa std::stringi inne zastosowania std::__cxx11::string). Jeśli twoje obiekty używają, std::stringzwykle powinny być skompilowane z tą samą implementacją łańcucha. Skompiluj z, -D_GLIBCXX_USE_CXX11_ABI=0aby wybrać oryginalną gcc4-compatibleimplementację lub -D_GLIBCXX_USE_CXX11_ABI=1wybrać nową cxx11implementację (nie daj się zwieść nazwie, może być również używany w C ++ 03, nazywa sięcxx11ponieważ spełnia wymagania C ++ 11). To, która implementacja jest domyślna, zależy od tego, jak skonfigurowano GCC, ale wartość domyślną można zawsze zastąpić makrem w czasie kompilacji.


„ponieważ kompilowanie jakiejkolwiek części programu z GCC 7 może wprowadzić zależności od symboli obecnych w libstdc ++. a więc z GCC 4.9 lub GCC 5” miałeś na myśli, że NIE są one obecne w GCC 4.9 lub GCC 5, prawda? Czy dotyczy to również linkowania statycznego? Dzięki za informacje o kompatybilności między wersjami kompilatora.
— Hadi Brais

1
Właśnie zdałem sobie sprawę z ogromnego błędu w oferowaniu nagrody za to pytanie. 😂
— Wyścigi lekkości na orbicie

4
@ricab Jestem w 90% pewien, że odpowiedź jest taka sama dla Clang / libc ++, ale nie mam pojęcia o MSVC.
— Jonathan Wakely

1
Ta odpowiedź jest niesamowita. Czy jest gdzieś udokumentowane, że wersja 5.0+ jest stabilna przez 11/14?
— Barry

1
Niezbyt wyraźnie lub w jednym miejscu. gcc.gnu.org/gcc-5/changes.html#libstdcxx i gcc.gnu.org/onlinedocs/libstdc++/manual/api.html#api.rel_51 deklarują, że obsługa bibliotek dla C ++ 11 jest kompletna (język obsługa była kompletna wcześniej, ale nadal „eksperymentalna”). Obsługa bibliotek C ++ 14 jest nadal wymieniana jako eksperymentalna do 6.1, ale myślę, że w praktyce nic się nie zmieniło między 5.x a 6.x, co wpływa na ABI.
— Jonathan Wakely

17

Odpowiedź składa się z dwóch części. Zgodność na poziomie kompilatora i zgodność na poziomie konsolidatora. Zacznijmy od tego pierwszego.

załóżmy, że wszystkie nagłówki zostały napisane w C ++ 11

Użycie tego samego kompilatora oznacza, że ​​ten sam nagłówek biblioteki standardowej i pliki źródłowe (elementy skojarzone z kompilatorem) będą używane niezależnie od docelowego standardu C ++. Dlatego pliki nagłówkowe biblioteki standardowej są napisane tak, aby były kompatybilne ze wszystkimi wersjami C ++ obsługiwanymi przez kompilator.

To powiedziawszy, jeśli opcje kompilatora użyte do kompilacji jednostki tłumaczeniowej określają konkretny standard C ++, to wszelkie funkcje, które są dostępne tylko w nowszych standardach, nie powinny być dostępne. Odbywa się to za pomocą __cplusplusdyrektywy. Zobacz plik źródłowy wektorowy , aby zobaczyć interesujący przykład jego użycia. Podobnie kompilator odrzuci wszelkie funkcje składniowe oferowane przez nowsze wersje standardu.

Wszystko to oznacza, że ​​twoje założenie może dotyczyć tylko napisanych przez ciebie plików nagłówkowych. Te pliki nagłówkowe mogą powodować niezgodności, jeśli są zawarte w różnych jednostkach tłumaczeniowych przeznaczonych dla różnych standardów C ++. Jest to omówione w załączniku C standardu C ++. Są 4 klauzule, omówię tylko pierwszą, a resztę krótko wspomnę.

C.3.1 Klauzula 2: konwencje leksykalne

Pojedyncze cudzysłowy ograniczają literał znaku w C ++ 11, podczas gdy są separatorami cyfr w C ++ 14 i C ++ 17. Załóżmy, że masz następującą definicję makra w jednym z czystych plików nagłówkowych C ++ 11:

#define M(x, ...) __VA_ARGS__

// Maybe defined as a field in a template or a type.
int x[2] = { M(1'2,3'4) };

Rozważ dwie jednostki tłumaczeniowe, które zawierają plik nagłówkowy, ale są przeznaczone odpowiednio dla C ++ 11 i C ++ 14. W przypadku języka C ++ 11 przecinek w cudzysłowie nie jest traktowany jako separator parametrów; jest tylko jeden parametr. Dlatego kod byłby równoważny z:

int x[2] = { 0 }; // C++11

Z drugiej strony, gdy celujemy w C ++ 14, pojedyncze cudzysłowy są interpretowane jako separatory cyfr. Dlatego kod byłby równoważny z:

int x[2] = { 34, 0 }; // C++14 and C++17

Chodzi o to, że użycie pojedynczych cudzysłowów w jednym z czystych plików nagłówkowych C ++ 11 może spowodować zaskakujące błędy w jednostkach tłumaczeniowych, które są przeznaczone dla C ++ 14/17. Dlatego nawet jeśli plik nagłówkowy jest napisany w C ++ 11, musi być napisany ostrożnie, aby zapewnić zgodność z późniejszymi wersjami standardu. __cplusplusDyrektywa może być tu przydatna.

Pozostałe trzy klauzule standardu to:

C.3.2 Rozdział 3: podstawowe pojęcia

Zmiana : nowy zwykły (bez miejsca docelowego) dezalokator

Uzasadnienie : Wymagane w przypadku zmniejszenia przydziału.

Wpływ na oryginalną funkcję : prawidłowy kod C ++ 2011 może zadeklarować globalną funkcję alokacji miejsca docelowego i funkcję cofania alokacji w następujący sposób:

void operator new(std::size_t, std::size_t); 
void operator delete(void*, std::size_t) noexcept;

Jednak w niniejszej Normie Międzynarodowej deklaracja usunięcia operatora może odpowiadać predefiniowanemu zwykłemu operatorowi usuwania (bez umieszczania) (3.7.4). Jeśli tak, program jest źle sformułowany, jak to miało miejsce w przypadku funkcji alokacji elementów klasy i funkcji cofania alokacji (5.3.4).

C.3.3 Klauzula 7: deklaracje

Zmiana : niestatyczne funkcje składowe constexpr nie są niejawnie stałymi funkcjami składowymi.

Uzasadnienie : Jest to konieczne, aby umożliwić funkcjom składowym constexpr mutowanie obiektu.

Wpływ na oryginalną funkcję : Prawidłowy kod C ++ 2011 może nie zostać skompilowany w tym standardzie międzynarodowym.

Na przykład poniższy kod jest prawidłowy w C ++ 2011, ale nieprawidłowy w tym standardzie międzynarodowym, ponieważ dwukrotnie deklaruje tę samą funkcję składową z różnymi typami zwracanych:

struct S {
constexpr const int &f();
int &f();
};

C.3.4 Klauzula 27: biblioteka wejścia / wyjścia

Zmiana : pobieranie nie jest zdefiniowane.

Uzasadnienie : używanie gadżetów jest uważane za niebezpieczne.

Wpływ na oryginalną funkcję : Prawidłowy kod C ++ 2011 korzystający z funkcji gets może nie zostać skompilowany zgodnie z niniejszą normą międzynarodową.

Potencjalne niezgodności między C ++ 14 i C ++ 17 są omówione w C.4. Ponieważ wszystkie niestandardowe pliki nagłówkowe są napisane w C ++ 11 (jak określono w pytaniu), te problemy nie wystąpią, więc nie będę o nich tutaj wspominał.

Teraz omówię kompatybilność na poziomie konsolidatora. Ogólnie rzecz biorąc, potencjalne przyczyny niezgodności obejmują:

Jeśli format wynikowego pliku obiektowego zależy od docelowego standardu C ++, konsolidator musi mieć możliwość łączenia różnych plików obiektowych. W GCC, LLVM i VC ++ na szczęście tak nie jest. Oznacza to, że format plików obiektowych jest taki sam, niezależnie od docelowego standardu, chociaż w dużym stopniu zależy od samego kompilatora. W rzeczywistości żaden z linkerów GCC, LLVM i VC ++ nie wymaga wiedzy o docelowym standardzie C ++. Oznacza to również, że możemy łączyć pliki obiektowe, które są już skompilowane (łącząc statycznie środowisko wykonawcze).

Jeśli procedura uruchamiania programu (funkcja, która wywołuje main) jest inna dla różnych standardów C ++ i różne procedury nie są ze sobą kompatybilne, wówczas nie byłoby możliwe dowiązanie plików obiektowych. W GCC, LLVM i VC ++ na szczęście tak nie jest. Ponadto podpis mainfunkcji (i ograniczenia, które się do niej stosuje, patrz sekcja 3.6 standardu) jest taki sam we wszystkich standardach C ++, więc nie ma znaczenia, w której jednostce tłumaczeniowej istnieje.

Ogólnie WPO może nie działać dobrze z plikami obiektowymi skompilowanymi przy użyciu różnych standardów C ++. Zależy to dokładnie od tego, które etapy kompilatora wymagają znajomości docelowego standardu, a które nie, oraz od wpływu, jaki ma on na optymalizacje międzyprocedurowe, które krzyżują się z plikami obiektowymi. Na szczęście GCC, LLVM i VC ++ są dobrze zaprojektowane i nie mają tego problemu (nie jestem tego świadomy).

Dlatego GCC, LLVM i VC ++ zostały zaprojektowane tak, aby umożliwić zgodność binarną z różnymi wersjami standardu C ++. Nie jest to jednak wymóg samej normy.

Nawiasem mówiąc, chociaż kompilator VC ++ oferuje przełącznik std , który umożliwia kierowanie na określoną wersję standardu C ++, nie obsługuje kierowania na C ++ 11. Minimalną wersją, którą można określić, jest C ++ 14, która jest domyślną wersją począwszy od Visual C ++ 2013 Update 3. Możesz użyć starszej wersji VC ++ do kierowania na C ++ 11, ale wtedy będziesz musiał użyć innych kompilatorów VC ++ kompilowanie różnych jednostek tłumaczeniowych, które są przeznaczone dla różnych wersji standardu C ++, co co najmniej złamałoby WPO.

UWAGA: Moja odpowiedź może nie być kompletna lub bardzo precyzyjna.


Pytanie dotyczyło raczej łączenia niż kompilacji. Uznaję (dzięki temu komentarzowi ), że być może nie był on jasny i zredagowałem go, aby wyjaśnić, że wszystkie zawarte nagłówki mają taką samą interpretację we wszystkich trzech standardach.
— ricab

@ricab Odpowiedź obejmuje zarówno kompilację, jak i tworzenie linków. Myślałem, że pytasz o jedno i drugie.
— Hadi Brais

1
Rzeczywiście, ale uważam, że odpowiedź jest o wiele za długa i myląca, zwłaszcza do „Teraz omówię kompatybilność na poziomie konsolidatora”. Możesz zastąpić wszystko powyżej czymś w rodzaju, jeśli nie można postulować, aby dołączone nagłówki miały to samo znaczenie w C ++ 11 i C ++ 14/17, to nie jest bezpieczne umieszczanie ich w pierwszej kolejności . Jeśli chodzi o pozostałą część, czy masz źródło wskazujące, że te trzy wypunktowane punkty są jedynymi potencjalnymi przyczynami niezgodności? W każdym razie dziękuję za odpowiedź, nadal głosuję
— ricab

@ricab Nie mogę powiedzieć na pewno. Dlatego na końcu odpowiedzi dodałem zastrzeżenie. Każdy może rozszerzyć odpowiedź, aby była bardziej precyzyjna lub kompletna na wypadek, gdyby coś przeoczyłem.
— Hadi Brais

To mnie wprawia w zakłopotanie: „Używanie tego samego kompilatora oznacza, że ​​będą używane te same standardowe nagłówki biblioteki i pliki źródłowe (...)”. Jak to możliwe? Jeśli mam stary kod skompilowany za pomocą gcc5, „pliki kompilatora” należące do tej wersji nie mogą być zabezpieczone w przyszłości. W przypadku kodu źródłowego skompilowanego w (szalenie) różnych czasach z różnymi wersjami kompilatora, możemy być prawie pewni, że nagłówek biblioteki i pliki źródłowe są różne. Zgodnie z zasadą, że powinny one być takie same, musisz przekompilować starszy kod źródłowy za pomocą gcc5, ... i upewnić się, że wszystkie używają najnowszych (tych samych) „plików kompilatora”.
— user2943111

2

Nowe standardy C ++ są podzielone na dwie części: funkcje językowe i komponenty bibliotek standardowych.

Jak rozumiesz przez nowy standard , zmiany w samym języku (np. Zakresowe dla) prawie nie stanowią problemu (czasami występują konflikty w nagłówkach bibliotek innych firm z nowszymi funkcjami języka standardowego).

Ale standardowa biblioteka ...

Każda wersja kompilatora zawiera implementację standardowej biblioteki C ++ (libstdc ++ z gcc, libc ++ z clang, standardowa biblioteka MS C ++ z VC ++, ...) i dokładnie jedną implementację, niewiele implementacji dla każdej standardowej wersji. Również w niektórych przypadkach możesz użyć innej implementacji biblioteki standardowej niż dostarczony przez kompilator. To, na co powinieneś zwrócić uwagę, to połączenie starszej implementacji biblioteki standardowej z nowszą.

Konflikt, który może wystąpić między bibliotekami stron trzecich a Twoim kodem, to biblioteka standardowa (i inne biblioteki), która łączy się z bibliotekami stron trzecich.


„Każda wersja kompilatora zawiera implementację STL” Nie, nie robią tego
— Lightness Races in Orbit

@LightnessRacesinOrbit Czy masz na myśli, że nie ma racji między np. Libstdc ++ i gcc?
— E. Vakili

8
Nie, mam na myśli to, że STL jest faktycznie przestarzały od ponad dwudziestu lat. Miałeś na myśli bibliotekę standardową C ++. Jeśli chodzi o resztę odpowiedzi, czy możesz przedstawić referencje / dowody na poparcie swojego roszczenia? Myślę, że w przypadku takiego pytania jest to ważne.
— Wyścigi lekkości na orbicie

3
Przepraszamy, nie, z tekstu nie wynika jasno. Zrobiłeś kilka interesujących twierdzeń, ale nie poparłeś ich jeszcze żadnymi dowodami.
— Wyścigi lekkości na orbicie
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.