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 ++)

ricab
źródło
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.

Jonathan Wakely
źródło
„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.

Hadi Brais
źródło
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.

E. Vakili
źródło
„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