Osiągnięcie kompatybilności z C ++ 11

12

Pracuję na dużej aplikacji, która musi działać na kilku platformach. Niektóre z tych platform obsługują niektóre funkcje C ++ 11 (np. MSVS 2010), a niektóre nie obsługują żadnych (np. GCC 4.3.x). Oczekuję, że ta sytuacja utrzyma się przez kilka lat (moje najlepsze przypuszczenia: 3-5 lat).

Biorąc to pod uwagę, chciałbym skonfigurować interfejs kompatybilności w taki sposób, aby (w możliwym stopniu) ludzie mogli pisać kod C ++ 11, który nadal będzie się kompilował ze starszymi kompilatorami przy minimalnej konserwacji. Ogólnie rzecz biorąc, celem jest zminimalizowanie # ifdef w jak największym stopniu, przy jednoczesnym włączeniu podstawowej składni / funkcji C ++ 11 na platformach, które je obsługują, i zapewnienie emulacji na platformach, które nie obsługują.

Zacznijmy od std :: move (). Najbardziej oczywistym sposobem osiągnięcia zgodności byłoby umieszczenie czegoś takiego we wspólnym pliku nagłówkowym:

#if !defined(HAS_STD_MOVE)
namespace std { // C++11 emulation
  template <typename T> inline T& move(T& v) { return v; }
  template <typename T> inline const T& move(const T& v) { return v; }
}
#endif // !defined(HAS_STD_MOVE)

To pozwala ludziom pisać takie rzeczy

std::vector<Thing> x = std::move(y);

... bezkarnie. Robi, co chce w C ++ 11 i robi to, co może najlepiej w C ++ 03. Kiedy w końcu upuszczamy ostatni kompilator C ++ 03, ten kod może pozostać taki, jaki jest.

Jednak zgodnie ze standardem wprowadzanie nowych symboli do stdprzestrzeni nazw jest nielegalne . To jest teoria. Moje pytanie brzmi: praktycznie mówiąc, czy jest coś złego w robieniu tego jako sposobu osiągnięcia kompatybilności z przyszłością?

mcmcc
źródło
1
Boost już zapewnia sporo tego i ma już kod do korzystania z nowych funkcji, kiedy / gdzie są dostępne, więc możesz być w stanie po prostu użyć tego, co zapewnia Boost, i możesz to zrobić. Oczywiście istnieją ograniczenia - większość nowych funkcji została dodana specjalnie dlatego, że rozwiązania oparte na bibliotece nie są odpowiednie.
Jerry Coffin
tak, szczególnie myślę o tych funkcjach, które można zaimplementować na poziomie biblioteki, a nie o zmianach składniowych. Boost tak naprawdę nie rozwiązuje problemu (płynnej) kompatybilności do przodu. Chyba że coś mi
umknie
Gcc 4.3 ma już kilka dobrych funkcji C ++ 11, przy czym prawdopodobnie referencje Rvalue są najważniejsze.
Jan Hudec
@JanHudec: Masz rację. Biedny przykład. W każdym razie istnieją inne kompilatory, które zdecydowanie nie obsługują składni (np. Dowolna wersja kompilatora IBM C ++, którą mamy).
mcmcc

Odpowiedzi:

9

Pracowałem od dłuższego czasu, utrzymując poziom kompatybilności do przodu i do tyłu w moich programach C ++, aż w końcu musiałem stworzyć z niego zestaw bibliotek , który przygotowuję do wydania, został już wydany. Zasadniczo, o ile zaakceptujesz, że nie uzyskasz „doskonałej” zgodności do przodu ani w funkcjach (niektórych rzeczy po prostu nie można emulować w przód) w składni (prawdopodobnie będziesz musiał użyć makr, alternatywnych przestrzeni nazw dla niektóre rzeczy), to wszystko gotowe.

Istnieje wiele funkcji, które można emulować w C ++ 03 na poziomie wystarczającym do praktycznego użycia - i bez wszystkich problemów związanych np. Z: Boost. Heck, nawet propozycja standardów C ++ nullptrsugeruje backport C ++ 03. Jest też TR1 na przykład dla wszystkiego C ++ 11-ale-mieliśmy-mieliśmy-zapowiedzi-przez lata. Ponadto niektóre funkcje C ++ 14, takie jak warianty asertywne, przezroczyste funktory, optional można zaimplementować w C ++ 03!

Jedyne dwie rzeczy, o których wiem, że nie mogą być absolutnie backportowane, to constexpr i szablony variadic.

Jeśli chodzi o całą kwestię dodawania rzeczy do przestrzeni nazw std, uważam, że to nie ma znaczenia - wcale. Pomyśl o Boost, jednej z najważniejszych i najistotniejszych bibliotek C ++ oraz ich implementacji TR1: Boost.Tr1. Jeśli chcesz ulepszyć C ++, uczyń go zgodnym z C ++ 11, a następnie z definicji przekształcasz go w coś, co nie jest C ++ 03, więc blokowanie się ponad standardem, którego i tak chcesz uniknąć , najprościej mówiąc, przynosi efekt przeciwny do zamierzonego. Puryści będą narzekać, ale z definicji nie trzeba się nimi przejmować.

Oczywiście, tylko dlatego, że nie będzie po (03) Standardowy przecież nie znaczy, że nie można próbować, czy pojedzie na wesoło obejść łamiąc go. Nie o to chodzi. Tak długo, jak zachowujesz bardzo ostrożną kontrolę nad tym, co jest dodawane do stdprzestrzeni nazw i masz kontrolę nad środowiskami, w których używane jest twoje oprogramowanie (tj.: Testuj!), Nie powinno być żadnej nieocenionej szkody. Jeśli to możliwe, zdefiniuj wszystko w osobnej przestrzeni nazw i dodaj usingdo niej tylko dyrektywy, aby stdnie dodawać niczego poza tym, co „absolutnie” musi wejść. Co, IINM, jest mniej więcej tym, co robi Boost.TR1.


Aktualizacja (2013) : jako żądanie pierwotnego pytania i widząc niektóre komentarze, których nie mogę dodać z powodu braku powtórzeń, oto lista funkcji C ++ 11 i C ++ 14 oraz ich stopień przenośności do C ++ 03:

  • nullptr: w pełni możliwe do wdrożenia, biorąc pod uwagę oficjalne zaplecze Komitetu; prawdopodobnie będziesz musiał podać także niektóre specjalizacje type_traits, aby został rozpoznany jako typ „rodzimy”.
  • forward_list: w pełni możliwe do wdrożenia, chociaż wsparcie dla alokatora zależy od tego, co może zapewnić implementacja Tr1.
  • Nowe algorytmy (kopiowanie partycji itp.): W pełni implementowalne.
  • Konstrukcje kontenerów z sekwencji nawiasów klamrowych (np .:) vector<int> v = {1, 2, 3, 4};: w pełni implementowalne, choć bardziej wyraziste niż byśmy tego chcieli.
  • static_assert: prawie w pełni możliwe do wdrożenia, gdy jest zaimplementowany jako makro (musisz tylko uważać na przecinki).
  • unique_ptr: prawie w pełni możliwe do wdrożenia, ale będziesz również potrzebować pomocy przy wywoływaniu kodu (do przechowywania ich w kontenerach itp.); patrz jednak poniżej.
  • referencje wartości: prawie w pełni możliwe do wdrożenia w zależności od tego, ile oczekujesz od nich (np .: Zwiększ ruch).
  • Iteracja Foreach: prawie w pełni możliwa do wdrożenia, składnia nieco się różni.
  • używanie funkcji lokalnych jako argumentów (na przykład: transform): prawie w pełni możliwe do wdrożenia, ale składnia będzie się wystarczająco różnić - na przykład funkcje lokalne nie są zdefiniowane w witrynie wywołania, ale tuż przed.
  • jawne operatory konwersji: możliwe do wdrożenia do praktycznych poziomów (jawna konwersja), zobacz „ express_castImperfect C ++ ; ale integracja z funkcjami językowymi, które static_cast<>mogą być prawie niemożliwe.
  • przekazywanie argumentów: możliwe do wdrożenia do praktycznych poziomów, biorąc pod uwagę powyższe informacje na temat odwołań do wartości, ale musisz zapewnić przeciążenie N funkcji, przyjmując argumenty, które można przekazywać.
  • move: możliwe do wdrożenia do praktycznych poziomów (patrz dwa wyżej). Oczywiście, aby skorzystać z tego, należy użyć kontenerów i obiektów modyfikujących.
  • Przydziały z zakresem: Naprawdę niemożliwe do wdrożenia, chyba że Twoja implementacja Tr1 może w tym pomóc.
  • typy znaków wielobajtowych: Naprawdę nie można go wdrożyć, chyba że Tr1 może cię wspierać. Ale w zamierzonym celu lepiej jest polegać na bibliotece specjalnie zaprojektowanej do radzenia sobie z tą sprawą, takiej jak OIOM, nawet jeśli używasz C ++ 11.
  • Listy argumentów variadic: możliwe do wdrożenia z pewnymi problemami, należy zwrócić uwagę na przekazywanie argumentów.
  • noexcept: zależy od funkcji kompilatora.
  • Nowa autosemantyka i decltype: zależy od funkcji twojego kompilatora - np __typeof__.:
  • typy liczb całkowitych wielkości ( int16_titp.): zależą od funkcji kompilatora - lub możesz przekazać je na Portable stdint.h.
  • atrybuty typu: zależy od funkcji kompilatora.
  • Lista inicjalizacyjna: Według mojej wiedzy nie można jej wdrożyć; jeśli jednak chcesz zainicjować kontenery sekwencjami, patrz wyżej na temat „konstrukcji kontenerów”.
  • Aliasing szablonów: nie do zrealizowania o mojej wiedzy, ale i tak jest to niepotrzebna funkcja, i mamy ::typeszablony na zawsze
  • Szablony Variadic: Nie do wdrożenia według mojej wiedzy; close jest domyślnie argumentem szablonu, który wymaga N specjalizacji itp.
  • constexpr: Nie można wdrożyć o mojej wiedzy.
  • Jednolita inicjalizacja: Nie można jej wdrożyć, ale gwarantowana domyślna inicjalizacja konstruktora może zostać zaimplementowana ala inicjalizacja wartości Boost.
  • C ++ 14 dynarray: w pełni implementowalny.
  • C ++ 14 optional<>: prawie w pełni możliwy do wdrożenia, o ile kompilator C ++ 03 obsługuje ustawienia wyrównania.
  • Przezroczyste funktory C ++ 14: prawie w pełni implementowalne, ale kod klienta prawdopodobnie będzie musiał jawnie użyć np .:, std::less<void>aby działał.
  • C ++ 14 nowych wariantów asercji (takich jak assure): w pełni implementowalne, jeśli chcesz aserty, prawie w pełni implementowalne, jeśli zamiast tego chcesz włączyć rzuty.
  • Rozszerzenia krotek C ++ 14 (pobierz element krotki według typu): w pełni implementowalne, a nawet nie uda się go skompilować z dokładnymi przypadkami opisanymi w propozycji funkcji.

(Oświadczenie: kilka z tych funkcji jest zaimplementowanych w mojej bibliotece backportów C ++, którą podłączyłem powyżej, więc myślę, że wiem o czym mówię, kiedy mówię „w pełni” lub „prawie w pełni”).

Luis Machuca
źródło
6

Jest to zasadniczo niemożliwe. Zastanów się std::unique_ptr<Thing>. Gdyby można było emulować odwołania do wartości jako bibliotekę, nie byłby to funkcja językowa.

DeadMG
źródło
1
Powiedziałem „w jak największym stopniu”. Oczywiście niektóre funkcje będą musiały zostać pozostawione za # ifdef lub w ogóle nie być używane. std :: move () to taki, który może obsługiwać składnię (choć nie funkcjonalność).
mcmcc
2
W rzeczywistości propozycja odwołań do wartości wspomina o rozwiązaniu opartym na bibliotece!
Jan Hudec
Mówiąc dokładniej, możliwe jest zaimplementowanie semantyki przenoszenia dla konkretnej klasy w C ++ 03, więc powinno być możliwe zdefiniowanie std::unique_ptrtam, ale niektóre inne cechy odwołań do wartości nie mogą być zaimplementowane w C ++ 03, więc std::forwardnie jest to możliwe. Inną rzeczą jest to, że std::unique_ptrnie będzie to przydatne, ponieważ kolekcje nie będą używać semantyki przenoszenia, chyba że zastąpisz je wszystkie.
Jan Hudec
@JanHudec: Nie można zdefiniować unique_ptr. Spójrz na wady auto_ptr. unique_ptrjest praktycznie podręcznikowym przykładem klasy, której semantyka została zasadniczo włączona przez funkcję języka.
DeadMG,
@DeadMG: Nie, nie jest tak, unique_ptrże funkcja języka została zasadniczo włączona. Bez tej funkcji nie byłby bardzo użyteczny. ponieważ bez idealnego przekazywania nie byłby on użyteczny w wielu przypadkach, a idealne przekazywanie wymaga tej funkcji.
Jan Hudec
2
  1. Gcc zaczął wprowadzać C ++ 11 (nadal C ++ 0x w tym czasie) w 4.3. Ta tabela mówi, że ma już odwołania do wartości i niektóre inne rzadziej używane funkcje (musisz określić -std=c++0xopcję, aby je włączyć).
  2. Wiele dodatków do standardowej biblioteki w C ++ 11 zostało już zdefiniowanych w TR1, a GNU stdlibc ++ udostępnia je w przestrzeni nazw std :: tr1. Więc po prostu wykonaj odpowiednie warunkowe użycie.
  3. Boost definiuje większość funkcji TR1 i może wstrzyknąć je do przestrzeni nazw TR1, jeśli go nie masz (ale VS2010 tak, a gcc 4.3 również, jeśli używasz GNU stdlibc ++).
  4. Umieszczanie czegokolwiek w stdprzestrzeni nazw jest „niezdefiniowanym zachowaniem”. Oznacza to, że specyfikacja nie mówi, co się stanie. Ale jeśli wiesz, że na konkretnej platformie biblioteka standardowa czegoś nie definiuje, po prostu idź i zdefiniuj to. Po prostu oczekuj, że będziesz musiał sprawdzić na każdej platformie, czego potrzebujesz i co możesz zdefiniować.
  5. Wniosek o referencje rvalue, N1690 wspomina jak zaimplementować semantykę poruszać się w C ++ 03. Można to zastąpić unique_ptr. Nie byłoby to jednak zbyt przydatne, ponieważ opiera się na kolekcjach wykorzystujących semantykę move, a C ++ 03 oczywiście nie.
Jan Hudec
źródło
1
Masz rację co do GCC, ale niestety muszę też obsługiwać inne kompilatory (inne niż GCC). Twoja kula nr 4 stanowi sedno pytania, które zadaję. # 5 jest interesujący, ale nie chcę obsługiwać semantyki przenoszenia (optymalizacji kopiowania) na tych starszych platformach, ale po prostu „std :: move ()” jako składni podlegającej kompilacji.
mcmcc