Aufwärtskompatibilität mit C ++ 11 herstellen

12

Ich arbeite an einer großen Softwareanwendung, die auf mehreren Plattformen ausgeführt werden muss. Einige dieser Plattformen unterstützen einige Funktionen von C ++ 11 (z. B. MSVS 2010) und andere nicht (z. B. GCC 4.3.x). Ich gehe davon aus, dass diese Situation mehrere Jahre anhält (meine beste Schätzung: 3-5 Jahre).

Vor diesem Hintergrund möchte ich eine Kompatibilitätsschnittstelle einrichten, mit der (soweit möglich) mit minimalem Wartungsaufwand C ++ 11-Code geschrieben werden kann, der auch mit älteren Compilern kompiliert werden kann. Insgesamt besteht das Ziel darin, # ifdefs so weit wie möglich zu minimieren, während die grundlegenden C ++ 11-Syntax / -Funktionen auf den Plattformen, die sie unterstützen, weiterhin aktiviert werden, und die Emulation auf den Plattformen bereitzustellen, die dies nicht tun.

Beginnen wir mit std :: move (). Der naheliegendste Weg, um Kompatibilität zu erreichen, wäre, so etwas in eine gemeinsame Header-Datei zu schreiben:

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

Dies ermöglicht es den Menschen, Dinge wie zu schreiben

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

... ungestraft. In C ++ 11 macht es, was es will, und in C ++ 03 macht es das Beste, was es kann. Wenn wir endlich den letzten C ++ 03-Compiler löschen, kann dieser Code unverändert bleiben.

Nach dem Standard ist es jedoch unzulässig, neue Symbole in den stdNamespace einzufügen. Das ist die Theorie. Meine Frage ist: Gibt es in der Praxis einen Schaden, wenn Sie dies tun, um die Vorwärtskompatibilität zu erreichen?

mcmcc
quelle
1
Boost bietet bereits einiges davon und verfügt bereits über Code, mit dem neue Funktionen verwendet werden können, wenn diese verfügbar sind. Daher können Sie möglicherweise nur das verwenden, was Boost bereitstellt, und damit fertig werden. Natürlich gibt es Einschränkungen - die meisten neuen Funktionen wurden speziell hinzugefügt, da bibliotheksbasierte Lösungen nicht ausreichend sind.
Jerry Coffin
Ja, ich denke speziell an die Funktionen, die auf Bibliotheksebene implementiert werden können, nicht an die syntaktischen Änderungen. Boost spricht das Problem der (nahtlosen) Aufwärtskompatibilität nicht wirklich an. Es sei denn, ich vermisse etwas ...
mcmcc
Gcc 4.3 hat bereits eine Handvoll C ++ 11-Features, wobei die Rvalue-Referenzen wahrscheinlich die wichtigsten sind.
Jan Hudec
@JanHudec: Du hast recht. Armes Beispiel. Auf jeden Fall gibt es andere Compiler, die die Syntax definitiv nicht unterstützen (z. B. welche Version von IBMs C ++ - Compiler wir auch haben).
mcmcc

Antworten:

9

Ich habe lange daran gearbeitet, die Vorwärts- und Rückwärtskompatibilität in meinen C ++ - Programmen beizubehalten, bis ich schließlich ein Bibliothekstoolkit daraus erstellen musste , das ich für die Veröffentlichung vorbereite. Es wurde bereits veröffentlicht. Im Allgemeinen, solange Sie akzeptieren, dass Sie keine "perfekte" Aufwärtskompatibilität erhalten, und zwar weder in Funktionen (einige Dinge können nur nicht in die Zukunft emuliert werden) noch in der Syntax (Sie müssen wahrscheinlich Makros oder alternative Namespaces für verwenden) einige Dinge), dann sind Sie fertig.

Es gibt viele Funktionen, die in C ++ 03 in einer Stufe emuliert werden können, die für den praktischen Gebrauch ausreicht - und das ohne den ganzen Aufwand, der damit verbunden ist, z. B .: Boost. Selbst der C ++ - Standardvorschlag für nullptrschlägt einen C ++ 03-Backport vor. Und dann gibt es zum Beispiel TR1 für alles, was mit C ++ 11 zu tun hat, aber wir haben schon seit Jahren Previews. Nicht nur das, einige C ++ 14- Features wie Assert-Varianten, transparente Funktoren und optional lassen sich in C ++ 03 implementieren!

Die einzigen zwei Dinge, von denen ich weiß, dass sie nicht unbedingt zurückportiert werden können, sind constexpr und variadic templates.

In Bezug auf das Hinzufügen von Inhalten zum Namespace stdbin ich der Ansicht, dass es überhaupt keine Rolle spielt . Denken Sie an Boost, eine der wichtigsten und relevantesten C ++ - Bibliotheken, und deren Implementierung von TR1: Boost.Tr1. Wenn Sie C ++ verbessern möchten, machen Sie es vorwärtskompatibel mit C ++ 11, und Sie wandeln es per Definition in etwas um, das nicht C ++ 03 ist. Blockieren Sie sich also über einen Standard, den Sie vermeiden oder den Sie ohnehin zurücklassen möchten Einfach gesagt, kontraproduktiv. Puristen werden sich beschweren, aber per definitionem braucht man sich nicht um sie zu kümmern.

Nur weil Sie dem (03) -Standard nicht folgen, heißt das natürlich nicht, dass Sie es nicht versuchen können, oder dass Sie es fröhlich umgehen werden , wenn Sie es brechen. Das ist nicht der Punkt. Solange Sie sehr sorgfältig kontrollieren, was dem stdNamespace hinzugefügt wird , und eine Kontrolle über die Umgebungen haben, in denen Ihre Software verwendet wird (z. B. Testen!), Sollte es überhaupt keinen unverträglichen Schaden geben. Wenn möglich, definieren Sie alles in einem separaten Namespace und fügen Sie nur usingDirektiven zum Namespace hinzu, stddamit Sie dort nichts hinzufügen, was "unbedingt" erforderlich ist. Was, IINM, ist mehr oder weniger das, was Boost.TR1 tut.


Update (2013) : Hier finden Sie eine Liste der C ++ 11- und C ++ 14-Funktionen sowie den Grad der Portabilität der ursprünglichen Frage und einiger Kommentare, die ich aufgrund mangelnder Repräsentanz nicht ergänzen kann zu C ++ 03:

  • nullptr: Vollständig umsetzbar angesichts des Rückstands des offiziellen Ausschusses; Sie müssen wahrscheinlich auch einige type_traits-Spezialisierungen angeben, damit es als "nativer" Typ erkannt wird.
  • forward_list: vollständig implementierbar, obwohl die Allokatorunterstützung davon abhängt, was Ihre Tr1-Implementierung bieten kann.
  • Neue Algorithmen (partition_copy usw.): vollständig implementierbar.
  • Containerkonstruktionen aus Klammer-Sequenzen (zB . :) vector<int> v = {1, 2, 3, 4};: vollständig implementierbar, wenn auch wortreicher als man möchte.
  • static_assert: Nahezu vollständig implementierbar, wenn es als Makro implementiert wird (Sie müssen nur mit Kommas vorsichtig sein).
  • unique_ptr: nahezu vollständig implementierbar, aber Sie benötigen auch Unterstützung beim Aufrufen von Code (zum Speichern in Containern usw.); siehe unten.
  • rWert-Referenzen: Nahezu vollständig implementierbar, je nachdem, wie viel Sie von ihnen erwarten (z. B .: Boost Move).
  • Für jede Iteration: Nahezu vollständig implementierbar, die Syntax wird sich etwas unterscheiden.
  • Verwendung lokaler Funktionen als Argumente (zum Beispiel: transform): Nahezu vollständig implementierbar, aber die Syntax wird sich ausreichend unterscheiden - zum Beispiel werden lokale Funktionen nicht am Aufrufstandort, sondern direkt davor definiert.
  • explizite Konvertierungsoperatoren: können auf praktische Ebenen implementiert werden (explizite Konvertierung), siehe Imperfect C ++ 's "explicit_cast"; Die Integration in static_cast<>Sprachfunktionen ist jedoch nahezu unmöglich.
  • Argumentweiterleitung: Implementierbar auf ein praktisches Niveau, wenn die oben genannten Werte für rvalue-Referenzen angegeben sind. Sie müssen jedoch N Überladungen für Ihre Funktionen bereitstellen, die weiterleitbare Argumente verwenden.
  • move: praktisch umsetzbar (siehe oben). Natürlich müssten Sie Modifikator-Container und -Objekte verwenden, um davon zu profitieren.
  • Bereichszuweiser: Nicht wirklich implementierbar, es sei denn, Ihre Tr1-Implementierung kann dies unterstützen.
  • Multibyte-Zeichentypen: Nicht wirklich implementierbar, es sei denn, Ihr Tr1 unterstützt Sie. Für den beabsichtigten Zweck ist es jedoch besser, sich auf eine Bibliothek zu verlassen, die speziell für die Behandlung der Angelegenheit entwickelt wurde, z. B. auf der Intensivstation, auch wenn C ++ 11 verwendet wird.
  • Variadische Argumentlisten: mit einigem Aufwand implementierbar, Argumentweiterleitung beachten.
  • noexcept: hängt von den Funktionen Ihres Compilers ab.
  • Neue autoSemantik und decltype: hängt von den Funktionen Ihres Compilers ab - z __typeof__.
  • Integer-Typen ( int16_t, usw.): hängt von den Funktionen Ihres Compilers ab - oder Sie können an die Portable-Datei stdint.h delegieren.
  • Typattribute: Abhängig von den Funktionen Ihres Compilers.
  • Initialisiererliste: Meines Wissens nach nicht implementierbar; Wenn Sie jedoch Container mit Sequenzen initialisieren möchten, lesen Sie die Informationen unter "Containerkonstruktionen".
  • Vorlagen-Aliasing: Meines Wissens nach nicht implementierbar, aber es ist ohnehin eine nicht benötigte Funktion, die wir schon ::typeimmer in Vorlagen hatten
  • Variadic templates: Meines Wissens nach nicht implementierbar; Der Abschluss ist das Standardvorlagenargument, für das N Spezialisierungen usw. erforderlich sind.
  • constexpr: Meines Wissens nach nicht implementierbar.
  • Einheitliche Initialisierung: Meines Wissens nach nicht implementierbar, aber eine garantierte Standardkonstruktor- Initialisierung kann ala Boost's value-initialized implementiert werden.
  • C ++ 14 dynarray: vollständig implementierbar.
  • C ++ 14 optional<>: Nahezu vollständig implementierbar, solange Ihr C ++ 03-Compiler Ausrichtungs-Setups unterstützt.
  • Transparente C ++ 14-Funktionen: Nahezu vollständig implementierbar, aber Ihr Client-Code muss wahrscheinlich explizit Folgendes verwenden:, std::less<void>damit es funktioniert.
  • C ++ 14 neue Assert-Varianten (wie z. B. assure): vollständig implementierbar, wenn Sie Asserts möchten, nahezu vollständig implementierbar, wenn Sie stattdessen Throws aktivieren möchten.
  • C ++ 14-Tupelerweiterungen (Tupelelement nach Typ abrufen): Vollständig implementierbar, und es kann sogar vorkommen, dass die Kompilierung mit den im Funktionsvorschlag beschriebenen Fällen fehlschlägt.

(Haftungsausschluss: Mehrere dieser Funktionen sind in meiner oben verlinkten C ++ - Backports-Bibliothek implementiert, sodass ich denke, dass ich weiß, wovon ich spreche, wenn ich "vollständig" oder "fast vollständig" sage.)

Luis Machuca
quelle
6

Das ist grundsätzlich unmöglich. Überlegen Sie std::unique_ptr<Thing>. Wenn es möglich wäre, R-Wert-Referenzen als Bibliothek zu emulieren, wäre dies keine Sprachfunktion.

DeadMG
quelle
1
Ich habe gesagt "zu welchem ​​Grad auch immer möglich". Es ist klar, dass einige Funktionen hinter den # ifdefs zurückbleiben müssen oder überhaupt nicht verwendet werden. Mit std :: move () können Sie die Syntax unterstützen (wenn auch nicht die Funktionalität).
mcmcc
2
Tatsächlich erwähnt der rvalue references-Vorschlag eine bibliotheksbasierte Lösung!
Jan Hudec
Insbesondere ist es möglich, die Verschiebungssemantik für eine bestimmte Klasse in C ++ 03 zu implementieren, so dass sie std::unique_ptrdort definiert werden sollte. Einige andere Funktionen von rvalue-Referenzen können jedoch nicht in C ++ 03 implementiert werden, sodass dies std::forwardnicht möglich ist. Die andere Sache ist, dass das std::unique_ptrnicht nützlich sein wird, da die Sammlungen die Verschiebungssemantik nur verwenden, wenn Sie sie alle ersetzen.
Jan Hudec
@JanHudec: Es ist nicht möglich zu definieren unique_ptr. Schauen Sie sich die Mängel an auto_ptr. unique_ptrist praktisch das Lehrbuchbeispiel einer Klasse, deren Semantik durch das Sprachmerkmal grundlegend ermöglicht wurde.
DeadMG
@DeadMG: Nein, es ist nicht so, unique_ptrdass die Sprachfunktion dies grundsätzlich ermöglicht hätte. Ohne diese Funktion wäre es jedoch nicht sehr nützlich. denn ohne perfekte Weiterleitung wäre es in vielen Fällen nicht verwendbar, und für eine perfekte Weiterleitung ist diese Funktion erforderlich.
Jan Hudec,
2
  1. Gcc hat mit der Einführung von C ++ 11 (damals noch C ++ 0x) in 4.3 begonnen. Diese Tabelle besagt, dass es bereits rWert-Referenzen und einige andere weniger genutzte Funktionen gibt (Sie müssen die -std=c++0xOption angeben , um sie zu aktivieren).
  2. Viele Ergänzungen zur Standardbibliothek in C ++ 11 wurden bereits in TR1 definiert, und GNU stdlibc ++ bietet sie im Namespace std :: tr1 an. Also mach einfach entsprechende Bedingungsanweisungen.
  3. Boost definiert die meisten TR1-Funktionen und kann sie in den TR1-Namespace einfügen, wenn Sie ihn nicht haben (VS2010 und gcc 4.3 ebenfalls, wenn Sie GNU stdlibc ++ verwenden).
  4. Das Einfügen von Elementen in den stdNamespace ist ein "undefiniertes Verhalten". Das heißt, die Spezifikation sagt nicht, was passieren wird. Wenn Sie jedoch wissen, dass die Standardbibliothek auf einer bestimmten Plattform etwas nicht definiert, definieren Sie es einfach. Erwarten Sie nur, dass Sie auf jeder Plattform überprüfen müssen, was Sie benötigen und was Sie definieren können.
  5. In N1690, dem Vorschlag für rWert-Referenzen, wird erwähnt, wie die Verschiebungssemantik in C ++ 03 implementiert wird. Das könnte als Ersatz dienen unique_ptr. Es wäre jedoch nicht allzu nützlich, da es sich auf Auflistungen stützt, die tatsächlich die Verschiebungssemantik verwenden, und die C ++ 03-Auflistungen offensichtlich nicht.
Jan Hudec
quelle
1
Sie haben Recht mit GCC, aber leider muss ich auch andere (Nicht-GCC-) Compiler unterstützen. Ihre Nr. 4-Kugel steht im Mittelpunkt der Frage, die ich stelle. # 5 ist interessant, aber ich versuche nicht, die Verschiebungssemantik (die Kopieroptimierung) auf diesen älteren Plattformen zu unterstützen, sondern nur "std :: move ()" als kompilierbare Syntax.
mcmcc