C++ neu denken: Architektur, Konzepte und Verantwortung
Warum der C++Builder 13 mehr als ein Versionssprung ist – und warum wir beginnen müssen, C++ wirklich zu verstehen.
Ein Artikel über den C++Builder – und doch über weit mehr
Mit dem Erscheinen der Version C++Builder 13 und der Unterstützung der aktuellen Standards C++20 und C++23 vor einigen Wochen und meinen darauf folgenden umfangreichen Tests entstand das Konzept einer neuen Bibliothek, und die Idee eines Buches. Damit natürlich auch dieser Text als eine erste Zusammenfassung. In den letzten Jahren habe ich immer wieder C++ Entwickler und Entwicklerinnen getroffen, die beim Blick auf modernes C++ nervös wurden, und oft hörte ich dann, dass dieses kein C++ wäre. Natürlich sieht modernes C++ anders aus, basiert viel mehr auf der Metaprogrammierung und hat sich zwar evolutionär entwickelt, dabei aber doch einen Quantensprung gemacht.
Bevor wir aber in das Thema einsteigen, möchte ich noch etwas allgemeines feststellen. Auf den ersten Blick scheint dieser Artikel sich auf den C++Builder, eine spezielle Entwicklungsumgebung zu beziehen. Tatsächlich betrifft er aber C++ im Ganzen: jeden Compiler, jede Plattform, jede Bibliothek für C++.
Auch wenn ich in diesem Artikel auf die speziellen Bibliotheken des C++Builder eingehe, wie VCL, FMX oder FireDAC, dass gleiche gilt uneingeschränkt für jede andere Bibliothek, zum Beispiel Qt. Ob Datenbank-, UI- oder Business-Framework, überall geht es um dasselbe Prinzip: modernes C++ als Sprache des Denkens, nicht des Nachbildens, zu verstehen.
So werden die Anforderungen an die jeweiligen Frameworks über Konzepte definiert, eine neue Art von “Vertrag”, die unsere Compiler überpüfen. Sie ermöglichen direkte und einfache Implementierungen der Schnittstellen ohne Laufzeit Overhead, schon zur Compile- Time.
Vereinfacht, ohne die Hilfskonzepte, sieht dieses Konzept für jede Form von Tabellenansichten (VCL zum Beispiel TListView, FMX ein TStringGrid, in Qt ein QTableWidget) folgendermaßen aus, und muss dann für die jeweiligen Komponenten / Widgets implementiert werden. In modernem C++ spielen Konzepte eine wichtige Rolle, werden von einigen sogar als eine Art neues Paradigma gesehen, der Konzept- basierten Entwicklung.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template <class backend_ty> concept table_type = check_get_all_opt<backend_ty, defined_values_types>::value && check_set_all_val<backend_ty, defined_param_types>::value && check_set_all_opt<backend_ty, defined_param_types>::value && requires(backend_ty a) { { a.rows() } -> std::convertible_to<std::size_t>; { a.columns() } -> std::convertible_to<std::size_t>; { a.insert_row() } -> std::convertible_to<std::size_t>; { a.current_row() } -> std::convertible_to<std::size_t>; { a.current_column() } -> std::convertible_to<std::size_t>; }; |
Kommen wir nach diesem Abstecher aber wieder zurück zum Thema. Die Analyse meiner Arbeit mit dem neuen C++Builder war für mich der Anlass, die Grenzen auszuloten, zu testen, was funktioniert, und bewusst herauszufinden, was (noch) nicht funktioniert. Nachdem die Migration unseres Hauptprodukts „PE Portal“ auf den C++Builder 13 bereits mit der Betaversion des neuen C++Builder überraschend gute Fortschitte machte, wollte ich nach der Veröffentlichung des offiziellen Release im September die Fähigkeiten zum Einsatz von C++20 und C++23 testen. Nach vielen eher schlechten Erfahrungen mit den Versionen 11 und 12 in den letzten Jahren wollte ich dieses Entwicklungssystem mit modernem C++ an den Rand der Möglichkeiten führen und habe anfangs anspruchsvolle Beispiele aus meinen Twitch- Streams genutzt.
Das Programmpaket „PE Portal“ besteht aus 10 Anwendungen, einige sind COM Server, und 30 dynamischen Bibliotheken. Bestehend aus 2312 Quelldateien (Source, Header, Ressourcen) und ungefähr 1 Mio. Quelltextzeilen. Die erfolgreiche Migration dauerte von Ende August bis Mitte Oktober 2025 (C++Builder 10.2.3 / DevExpress 23.2.7 (32bit) auf C++Builder 13 / DevExpress 25.1.5 (64bit)).
Zu meiner eigenen Überraschung habe ich dabei selbst neue Horizonte erreicht und mein Wissen über C++ weiter vertieft.
Je mehr ich ausprobierte, je mehr ich die Sprache ausreizte, und dabei die verrücktesten Ideen umsetzte, desto klarer wurde mir, dass modernes C++ nicht mehr gelernt (oder gelehrt) werden kann, sondern vom Grunde her neu begriffen werden muss. Ich hatte ja vor einigen Wochen in meinen Blog geschrieben, wie wichtig der Wechsel von C++17 auf C++20 / C++23 ist.
Als Mathematiker bin ich abstraktes Denken gewohnt, und mir war die Idee der Metaprogrammierung natürlich schon früh vertraut. Ich habe es nicht nur aktiv genutzt, sondern auch Schulungen zur Metaprogrammierung angeboten. Ich hatte also schon eine Vorstellung, dass Programme nicht nur Abläufe steuern, sondern selbst Modelle beschreiben, die sich zur Compile-Time formen und verändern können. So habe ich zum Beispiel in einem Stream im Dezember 2022 auch das Verfahren “tie the knot” mit Metaprogrammierung und Foldering zur Compile-time erklärt (mit Visual Studio 2022) mit dem ich schon tief in die Metaprogrammierung eingetaucht bin.
Was heute mit concept und variadic template möglich geworden ist, konnte ich nicht erahnen.
Heute, im Lichte der neuen Standards C++20 und C++23, wird genau dieses Denken Realität. Und der evolutionäre Weg der Standards ab C++11 setzt sich zielgerichtet fort. C++ hat eine neue Qualität erreicht, und neben dem Compile-time Paradigma, über das schon viel geschrieben wurde, gibt es jetzt concept- basierendes Programmieren.
Die Renaissance des Denkens in modernem C++
C++ steht an einem historischen Wendepunkt. Dieser wird nur gelingen, wenn wir uns auf C++ einlassen, und bereit sind, wieder zu lernen. Die Sprache, einst das Werkzeug für systemnahes und maschinennahes Programmieren, ist zu einem architektonischen Medium geworden: einem Instrument, mit dem sich Strukturen, Beziehungen und Lebenszyklen nicht nur formulieren, sondern als Entwurfslogik ausdrücken lassen.
Mit den Sprachstandards seit C++11 – beginnend mit der move-Semantik über variadic templates, der bessere Nutzung von SFINAE, funktionale Erweiterungen und Lambda Funktionen, bis hin zu std::thread und std::async, C++ wurde zu einer Sprache, die neben mehr Effizienz auch Kontrolle über Parallelität und Nebenläufigkeit bietet, und eine neue Tiefe vorbereitet. Es waren die erste Schritte auf dem evolutionären Weg, C++20 hat diese Entwicklung mit dem kooperativen Multitasking über Generatoren und Co-Routinen vollendet: Pipes und Ranges ergänzen sich zu einem Modell, in dem Datenflüsse nicht mehr beschrieben, sondern deklariert werden. Die in C++11 eingeführten type traits finden in den concept den Abschluss und ermöglichen einen völlig neuen Programmierstil, das concept based programmieren.
Heute geht es nicht mehr darum, Funktionen zur Laufzeit auszuführen, sondern Systeme zu entwerfen, die sich selbst beschreiben, validieren und bereits zur Compile-Time optimieren. Der Compiler ist dann kein reiner Übersetzer mehr, sondern ein Partner in der architektonischen Konstruktion.
Vom Anwender zum Architekten
Viele Entwickler und Entwicklerinnen glauben, dass es auch zukünftig ausreichen wird, Anwender zu bleiben, die Sprache und damit auch Bibliotheken zu nutzen, Frameworks einzubinden und bewährte Muster fortzuführen. Dabei verharren sie oft in den Paradigmen der 1990er Jahre. Doch das genügt nicht, wenn man die Zukunft von C++ gestalten will. Die Architekten und Architektinnen sind gefragt, C++ neu zu denken. Und die Trainer und Trainerinnen, selber neu zu lernen und neue Konzepte zu erarbeiten.
Die Hersteller von Bibliotheken müssen den Mut haben, eine neue Generation von Bibliotheken zu schaffen. Bibliotheken, die concepts, Typlisten, Ranges und Compile-Time-Mechanismen konsequent einsetzen. Die Compilerhersteller wiederum stehen in der Verantwortung, diese Entwicklung fortzusetzen und die neuen Sprachmittel vollständig zu erschließen.
Aber auch wir alle, alle C++-Entwickler und Entwicklerinnen, müssen wieder auf die Schulbank. Wir müssen C+++ neu lernen, nicht weil wir es verlernt hätten, sondern weil es durch die Evolution zu einer anderen Sprache geworden ist. Nur wer die modernen Sprachkonstrukte versteht, kann die neuen Werkzeuge richtig nutzen und das Potenzial dieser Generation von Bibliotheken entfalten.
Der Wert der Übersetzungszeit
Viele, insbesondere historisch denkende Entwickler, beklagen sich darüber, dass moderne C-Compiler immer länger übersetzen. Doch diese Kritik ist kurzsichtig. Man darf die Übersetzungszeit von C+++ nicht mit der Kompilierung in anderen Sprachen vergleichen, weil der Compiler hier etwas völlig anderes leistet. Und das liegt nicht nur an einer guten Optimierung des ausführbaren Maschinencodes. Während andere moderne Sprachen meist eher auf das funktionale Paradigma setzen, dass C++ als multiparadigma Sprache natürlich auch unterstützt, steht hier aber die Metaprogrammierung im Vordergrund.
Wir entwerfen in modernem C++ Blaupausen für den Compiler, präzise, generische, konzeptionelle Modelle. Der Compiler prüft diese Strukturen, kombiniert sie, spezialisiert sie und erzeugt daraus hochoptimierten Code. Dabei berechnet er Methoden und Konstanten bereits zur Compile-Time, wenn sie zeitinvariant sind. Was also scheinbar die Übersetzungszeit verlängert, reduziert in Wirklichkeit die Entwicklungszeit und die Laufzeit. Und es erhöht die Zuverlässigkeit.
C++-Kompilierung ist keine bloße Syntaxumwandlung, sondern ein Teil der Ausführung. Der Compiler ist nicht langsamer, sondern intelligenter geworden. Die gewonnene Zeitverschiebung, von der Laufzeit in die Übersetzungsphase, ist Ausdruck einer Sprache, die zukünftig in Leistung und Sicherheit investiert.
Ich erkläre seit vielen Jahren, wer C++ wie eine andere Sprache nutzt, sei es Delphi oder Java / C#, sollte besser diese Sprache verwenden. Schon weil diese schneller compilieren und die eigentlichen Vorteile von C++ jenseits der Sprachmittel dieser Sprachen liegen.
C++Builder: C++ in einem Tool – oder mit einem Tool?
In den letzten Wochen habe ich in meinen Streams den neuen C++Builder 13 intensiv live getestet. Jeder konnte es verfolgen. Dabei wollte ich nicht C++ in einem Tool verwenden, sondern mit einem Tool C++ leben. Das ist ein sehr entscheidender Unterschied.
Die Delphi Bibliotheken VCL und FMX bieten seit Jahren eine beeindruckende Basis für grafische Oberflächen, intuitiv, stabil, leistungsfähig. FireDAC stellt eine der elegantesten Datenbank-Konnektivitäten bereit, die bereits zur Entwurfszeit funktioniert. Doch das genügt heute nicht mehr: die nächste Generation von C++ will nicht nur benutzt, sondern neu gedacht werden.
Ich habe bewusst Grenzen gesucht, dort, wo das moderne C++ im Zusammenspiel mit dem C++Builder noch unvollständig wirkt. Diese Grenzsuche wurde kein Testbericht, sondern eine Entdeckungsreise. Je mehr ich die Sprache in der Praxis ausreizte, desto deutlicher erkannte ich die Richtung, in die sich C++ Bibliotheken und Anwendungen entwickeln müssen.
Als Mathematiker sehe ich darin eine logische Konsequenz: Metaprogrammierung ist nichts anderes als angewandte Theorie. Das Denken in Modellen, die sich selbst beschreiben und auswerten. Genau das wird durch modernes C++ endlich möglich.
Sicherheit durch RAII: Ressourcen sind Verantwortung
Ein zentrales Prinzip dieser neuen Denkweise ist RAII (Resource Acquisition Is Initialization). RAII steht schon lange im Blickpunkt für sichere Programmierung. Es steht für die Einsicht, dass Sicherheit mehr bedeutet als Speicherverwaltung, nämlich die stikte Kontrolle über jede Ressource.
In meinen Streams habe ich gezeigt, wie sich RAII dadurch auch auf Datenbankoperationen anwenden lässt: Eine Verbindung besteht, solange sie sich im Scope befindet. Eine Datenbank-Abfrage liefert Daten, solange ihr Objekt lebt. Scope ist keine syntaktische Grenze, sondern ein semantisches Versprechen, eine Garantie für Integrität.
Diese deterministische Kontrolle ersetzt Zufall durch Ordnung. Ressourcen sind nicht temporär, sie sind verantwortet. Natürlich nimmt einem Sprache die Verantwortung nicht ab, aber sie bietet Werkzeuge. C++ verknüpft Objektlebensdauer und Systemstabilität, eine Eigenschaft, die in anderen Sprachen in dieser Präzision selten existieren.
Konzepte und Ranges und Auswertungen zur Übersetzungszeit
Das Konzept der concept in C++ dient der präzisen Formulierung von Typanforderungen und führt so zu einer klareren, besser überprüfbaren generischen Programmierung. Während Templates im klassischen C++ oft erst bei der Instanziierung mit langen Fehlermeldungen versagen, erlauben Concepts eine deklarative Definition dessen, was ein Typ können oder bereitstellen muss. Damit schließen sie nahtlos an den Grundgedanken der Standard Template Library (STL) an, deren Stärke in der Trennung von Algorithmus und Datenstruktur liegt: ein Algorithmus beschreibt den Ablauf, ein Iterator abstrahiert den Zugriff auf die Daten, und beides wird über Typkompatibilität lose gekoppelt. Aus dieser Kombination entstand die große Wiederverwendbarkeit der STL-Algorithmen, die unabhängig vom konkreten Container arbeiten. Mit C++20 erweitert std::ranges dieses Prinzip entscheidend: es integriert die Begriffe Iterator, Container und Algorithmus zu einem einheitlichen Modell, das Typsicherheit und Lesbarkeit verbindet. Statt Iteratoren manuell zu handhaben, operieren Ranges direkt auf Sequenzen und ermöglichen durch Filter, Transformationen und Views eine deklarative, pipelineartige Beschreibung von Datenverarbeitung, präzise, typsicher und konzeptionell klar. Und trotzdem flexible und wiederverwendbar.
Im klassischen C++ (und der frühen Phase der STL) galt es als guter Stil, jede Datenhaltung und jedes Iterationsziel in Form eines Containers zu modellieren: Listen, Vektoren oder Maps bildeten die Grundlage für nahezu jede algorithmische Operation. Diese Denkweise machte die Trennung von Daten und Verarbeitung konsequent, führte jedoch oft zu unnötigen Kopien oder komplexen Adapterkonstruktionen, sobald nur Teilbereiche oder gefilterte Sichten benötigt wurden. Mit der Einführung von std::ranges hat sich dieses Paradigma grundlegend verschoben. Ranges übernehmen im modernen C++ die Aufgabe eines universellen Transportmediums zwischen Datenquelle und Algorithmus: sie können reale Container, Iteratorpaare oder dynamisch erzeugte Sequenzen abbilden, ohne selbst Daten zu besitzen. Damit wird die Sicht auf Daten vom Besitz entkoppelt, Operationen werden oft lazy ausgewertet und können zu effizienten Verarbeitungspipelines kombiniert werden. Durch diese Eigenschaften sind Ranges zu einem zentralen Baustein modernen C++ geworden, sie ermöglichen einen funktionalen, deklarativen Stil, der zugleich effizient und ausdrucksstark ist, und bilden damit den logischen Nachfolger der klassischen Containerabstraktion.
Die Verbindung von Ranges und Konzepts eröffnet in modernem C++ eine neue Qualität der Datenbearbeitung: typsicher, generisch und zugleich hochgradig effizient. Während concepts exakt definieren, welche Fähigkeiten ein Typ bereitstellen muss, bilden ranges die flexible Infrastruktur, um Datenquellen jeder Art, egal ob Container, Iteratorbereiche, Generatoren oder zusammengesetzte Pipelines, in einheitlicher Form zu verarbeiten. Diese Kombination erlaubt es, Algorithmen so zu formulieren, dass sie nur für passende Typen instanziiert werden, wodurch Fehler bereits zur Compilezeit erkannt werden. In Verbindung mit variadic templates lassen sich zudem ganze Sequenzen heterogener Typen typensicher verarbeiten, beispielsweise durch Parameterpack-Expansions oder fold expressions, die auf jedem Element einer Ranges-ähnlichen Struktur operieren. Damit entsteht ein generisches Datenverarbeitungsmodell, das sowohl speichereffizient als auch optimierungsfreundlich ist: unnötige Kopien entfallen, Auswertungen erfolgen lazy und der Compiler kann die gesamte Pipleline prüfen und zu optimierten Maschinencode erzeugen.
Das Ergebnis ist eine Sprachebene, in der Typsicherheit, Ausdruckskraft und Laufzeiteffizienz nicht mehr im Widerspruch stehen, sondern sich gegenseitig verstärken.
Delphi Typen als concept und std::ranges
In dem folgenden Abschnitt geht es um eine Verbindung von modernem C++ und Delphi. Ich möchte sowohl für die VCL als auch FMX std::ranges Typen für TMemo- Komponenten haben, aber auch auf TListbox Komponenten oder ähnliches zugreifen können. Das Ziel ist eine typsichere, zur Compilezeit gesteuerte und zur Laufzeit abgesicherte Verbindung zwischen den beiden Sprachen, die ausschließlich valide Owner akzeptiert und andernfalls präzise Diagnosen liefert. Dazu schaffen wir erst einmal die Grundlagen und binden C++ Konzepte ein und greifen auf die Delphi RTL Klassen zurück.
Wir verwenden nicht die unterschiedlichen Bibliotheken und nicht die konkreten Eigenschaften
Die Kapselung im eigenen Namespace hält die Oberfläche klar und verhindert Kollisionen, der Inhalt dieses Beispiels versteht sich als Baustein, den Sie in bestehende Projekte integrieren können. Als leichtgewichtiger Anker dient ein Shim- Klasse, die ausschließlich eine TStrings Quelle bereitstellt, die als grundlegende Eigenschaft in den oben genannten Komponenten die Daten speichert. Diese Hülle erlaubt es später, auch Nicht-TComponent-Typen in die gleiche Konzeptlogik einzubinden, ohne die Semantik zu überdehnen: der Shim bildet lediglich die Eigenschaft ab, nicht mehr und nicht weniger.
|
1 2 3 4 5 6 7 8 9 10 |
#include <concepts> #include <System.Classes.hpp> #include <System.SysUtils.hpp> namespace adecc::delphi { struct LinesOwnerShim { System::Classes::TStrings* Lines{}; }; |
Das folgende Konzept ist eine Compile-time -Verifikation, um Typen zu definieren und zu erkennen, die aus der Delphi Komponenten- Welt stammen. Das folgende Konzept erkennt Komponenten, die von System::Classes::TComponent ableiten. Der Nutzen liegt in der präzisen Kandidatenauswahl: nur echte Komponenten oder der vorher definierte explizite Shim dürfen später als Owner auftreten.
|
1 2 |
template <typename ty> concept DelphiComponent = std::derived_from<ty, System::Classes::TComponent>; |
Für die eigentliche Speicherung der Daten in den Komponenten, für die für uns interessieren, besitzen ein Property Lines (z.B. TMemo) oder Items (z.B. TListBox), die jeweils den Zugriff auf eine TStrings* anbietet. Aus Sicht von C++ reicht es sogar, wenn diese Eigenschaften nur zu dem Typ konvertierbar sind. Auch wenn C++ selber keine Properties kennen, verhalten sie sich wie eine Variable die lesbar und schreibbar sein können.
Diese Anforderungen lassen sich in zwei kleine, orthogonale Konzepte formulieren. Damit bleibt die Diagnose für den Compiler granular: fehlt Lines, prüft der Compiler noch Items und meldet erst dann ein klares Negativ.
|
1 2 3 4 5 6 7 8 9 10 11 |
template <typename ty> concept HasLinesProp = requires(ty* p) { { p->Lines } -> std::convertible_to<System::Classes::TStrings*>; }; template <typename ty> concept HasItemsProp = requires(ty* p) { { p->Items } -> std::convertible_to<System::Classes::TStrings*>; }; |
Die beiden Konzepte verbinden wir zu einem gemeinsamen.Zugelassen sind demnach entweder die vorher definierte LinesOwnerShim Klasse oder eine Delphi-Komponente, die mindestens eine der beiden Eigenschaften bereitstellt. Die Logik bleibt dabei bewusst streng: weder „fast passend“ noch implizite Konvertierungen außerhalb von TStrings* werden akzeptiert.
|
1 2 3 4 |
template <typename ty> concept HasLinesOrItems = (std::same_as<std::remove_cvref_t<ty>, LinesOwnerShim> || DelphiComponent<ty>) && (HasLinesProp<ty> || HasItemsProp<ty>); |
Software ändert sich, insbesondere auch Komponenten aus Fremdbibliotheken. Für diese wichtigen Fall müssen die logischen Folgen in der Metaprogrammierung immer abgesichert werden. Auch das passiert schon zur compile-time und wir brauchen einen Prüfwert der immer false liefert. Die Hilfskonstante always_false_v erlaubt es, in abhängigem Kontext zuverlässig eine Übersetzungszeit-Fehlermeldung auszulösen, ohne dabei unnötig Templates zu instantiieren.
|
1 2 |
template <typename> inline constexpr bool always_false_v = false; |
Ich möchte jetzt nicht die gesamte Klasse mit Iterator, einem Typ um die Zeilen im TStrings* zu std::string zu machen, dem std::range und den backinserter angegeben. Deshalb nur die Klassendeklaration und eine statische Methode, die die Zuordnung zur compile-time übernimmt.
Der Template-Parameter wird durch das obige Konzept HasLinesOrItems eingeschränkt, die statische Methode ValidateAndGetSequence liefert eine geprüfte TStrings* mit den Daten. Das Vorgehen ist zweistufig: zur compile-time die Auswahl der passenden Eigenschaft mit zur Laufzeit nachgelagerter Nullprüfung. So sind sowohl falsch verdrahtete Owner als auch zur Laufzeit fehlende Objekte sauber abgefangen.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template <HasLinesOrItems owner_ty> class LinesView { public: static System::Classes::TStrings* ValidateAndGetSequence(owner_ty* const pOwner) { if(!pOwner) throw std::invalid_argument("LinesView: owner null"); if constexpr (HasLinesProp<owner_ty>) { if(!pOwner->Lines) throw std::invalid_argument("LinesView: Lines null"); return pOwner->Lines; } else if constexpr (HasItemsProp<owner_ty>) { if(!pOwner->Items) throw std::invalid_argument("LinesView: Items null"); return pOwner->Items; // bereits TStrings* } else { static_assert(always_false_v<owner_ty>, "Owner has neither Lines nor Items"); } } }; |
Damit ist die Brücke zwischen C++ und Delphi komplett: die Konzepte definieren die zulässigen Typen präzise, die Methode wählt den richtigen Zugriffspfad deterministisch zur Compilezeit und sichert zur Laufzeit gegen Null ab. In der Praxis ergibt sich daraus ein robustes Muster für Komponenten wie TMemo, TListBox oder eigene Adapter, die TStrings über Lines oder Items bereitstellen.
Typsicherheit, Varianten und mehrwertige Logik
Mit std::variant erhielt C++ ein Werkzeug, das dynamische Zustände erlaubt, ohne die Typsicherheit aufzugeben. Varianten sind keine unkontrollierten, dynamischen Container, sondern definierte Mengen möglicher Typen, die explizit abgefragt werden können.
Doch das Denken endet hier nicht: std::optional hat eine neue Tiefe eröffnet, die einer mehrwertigen Logik entspricht. Ein Wert kann existieren oder nicht existieren. Diese Möglichkeit, dass eine Variable keinen Wert besitzt, war für viele Entwickler lange unvorstellbar. Mit std::expected wurde dieses Konzept erweitert: Ein Ergebnis kann gültig sein oder einen Fehlerzustand tragen, und beide Fälle sind Teil des Typensystems.
In dieser monadischen Sichtweise wird aus der Fehlerbehandlung ein Aspekt der Semantik. Optionale oder erwartete Werte sind keine Sonderfälle mehr, sondern logische Zustände: mathematisch präzise und vom Compiler überprüfbar.
Auf dieser Grundlage eröffnen concepts eine neue Ebene der Sicherheit. Nur ein Beispiel. Wir können Variablen über enum class-Typen und concept-basierte Parameter mit expliziten Einheiten versehen: Meter, Sekunden, Grad, Pfund oder Newton. So entsteht ein System, das automatische, aber kontrollierte Umrechnungen erlaubt, etwa zwischen metrischen und imperialen Maßen und zur Compile-Time validiert, ob eine Operation semantisch sinnvoll ist.
Das gleiche kann man auch mit Grad und Bogenmaß machen, dieses ist vielleicht eher verständlich
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace adecc { enum class AngleKind : int { degree, radian }; template<typename ty> inline constexpr bool always_false_angle = false; template <typename ty> concept param_angle = std::is_floating_point_v<ty> || (std::is_integral_v<ty> && !std::is_same_v<ty, bool>); template <typename ty, AngleKind kind> concept param_angle_valid = (kind == AngleKind::degree) || std::is_floating_point_v<ty>; template <param_angle ty = double, AngleKind kind = AngleKind::degree> requires param_angle_valid<ty, kind> class TAngle { private: ty theAngle; ... }; } |
Ich möchte zur Erklärung nicht die ganze Klasse zeigen, aber ein Beispiel. Der Winkel soll korrekt ausgegeben werden, wahlweise natürlich mit einem abweichenden Typ. Dabei kann der Wert als Referenz zurückgegeben werden, wenn die Art des Winkels und der verwendete Datentyp übereinstimmen, sonst muss eine Umrechnung erfolgen, und die Rückgabe erfolgt als Wert. Hier ermöglicht SFINAE genau dieses, und man kann erkennen, dass man heute in C++ nur wenig dem Zufall überlassen muss. Und jeder sollte erkennen, dass dieses nicht zur Laufzeit passiert.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template <AngleKind other_knd = kind, angle_param other_ty = ty> constexpr std::conditional_t<(other_knd == kind) && std::is_same_v<other_ty, ty>, ty const&, ty> Angle(void) const { if constexpr ((other_knd == kind) && std::is_same_v<other_ty, ty>) return theAngle; else if constexpr (other_knd == AngleKind::degree && kind == AngleKind::radian) { if constexpr (std::is_same_v<other_ty, ty>) return theAngle * 180.0 / std::numbers::pi_v<ty>; else return static_cast<other_ty>(theAngle * 180.0 / std::numbers::pi_v<ty>); } else if constexpr (other_knd == AngleKind::radian && kind == AngleKind::degree) { if constexpr (std::is_same_v<other_ty, ty>) return theAngle * std::numbers::pi_v<ty> / 180.0; else static_cast<other_ty>(theAngle * std::numbers::pi_v<ty> / 180.0); } else static_assert(always_false_angle<kind>, "invalid kind of angle"); } |
Wenn wir jetzt trigonometrische Funktionen nutzen wollen (std::sin, std::cos, …) benötigen wir den Winkel im Bogenmaß, vielleicht hat jemand von Ihnen dieses schon einmal vergessen und einen Programmfehler erzeugt. Deshalb können wir ähnlich zum oberen Beispiel eine Methode toRadiananbieten, und dann für diese Klasse eigene, sichere Methoden anbieten. Dabei kann die Methode toRadian eine Referenz zurückgeben, wenn der Winkeltyp und Datentyp gleich sind, sonst muss ein Wert zurückgegeben werden. Hier bietet uns C++ schon lange SFINAE.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
template <typename other_ty = ty> std::conditional_t<(kind == AngleKind::radian && std::is_same_v<ty, other_ty>), other_ty const&, other_ty> toRadians(void) const { if constexpr (kind == AngleKind::radian) { if constexpr (std::is_same_v<ty, other_ty>) return Angle(); else return static_cast<other_ty>(Angle()); } else if constexpr (kind == AngleKind::degree) { if constexpr (std::is_same_v<ty, other_ty>) return Angle() * std::numbers::pi_v<ty> / 180.0; else return static_cast<other_ty>(Angle() * std::numbers::pi_v<ty> / 180.0); } else { static_assert(always_false_angle<kind>, "invalid kind of angle in toRadian"); } } template <std::floating_point other_ty = ty> auto sin(void) const { if(std::is_same_v<ty, other_ty>) return std::move(std::sin(toRadians())); else return static_cast<other_ty>(std::sin(toRadians())); } |
Und natürlich können wir für diesen neuen Typ einen std::formatter definieren, um auch die Einheit auszugeben, und dabei die normalen Formatregeln für Zahlen zu unterstützen. Ausserdem Stringliterale, um auch mit den Einheiten auch direkt im Quelltext zu arbeiten.
Diese Fähigkeit ist revolutionär, denn sie bricht mit einem jahrzehntelangen Dogma: In Programmen wurde nie mit „Äpfeln, Birnen und Tortenstücken“ gerechnet, sondern mit abstrakten Zahlen. Das war eine Denkweise, die zu unzähligen Problemen und Katastrophen führte, bis hin zur gescheiterten Mars Climate Orbiter-Mission, bei der der Mischgebrauch von Einheiten den Verlust einer Raumsonde verursachte.
Mit modernem C++ können wir diese Fehlerklasse beseitigen. Durch concept-basiertes Denken entstehen Typsysteme, die physikalische Einheiten, Maße und Werte trennen und gleichzeitig rechnerisch verbinden. Wir können „sichere Zahlen“ definieren, die keine unkontrollierten Umwandlungen mehr erlauben. Die Sprache selbst wird damit zum Wächter über Logik und Konsistenz. Ein Fortschritt, den frühere Generationen von Entwickelnden nur erträumen konnten.
Typlisten, Templates und Kompilierzeit-Sicherheit
Ein weiteres Beispiel. In meinen Experimenten zu Datenbankwerten und Parametern habe ich variadic templates und fold expressions genutzt, um Typlisten zu definieren, die bereits zur Compile-Time überprüfen, welche Wertebindungen erlaubt sind. Wenn eine SQL-Abfrage Parameter bindet, entscheidet der Compiler über Richtigkeit und Reihenfolge, nicht die Laufzeit. Für die Umwandlungen der unterschiedlichen Typen sorgen Trait Strukturen.
Dieses Prinzip erweitert die Idee der Sicherheit von RAII auf die Ebene der Typen. Es schafft ein System, in dem Fehler nicht mehr entstehen können, weil sie zur Kompilierzeit ausgeschlossen werden. Das folgende Beispiel zeigt eine einfache Typliste, mit einem std::tuple Typ um eine vollständige Typliste zu halten, und einen std::variant, als Typ, um einen einzelnen Wert aus der Liste zu speichern zu können. Die Methode invoke dient dazu, mehrere Operationen in einer Methode mit der gleichen Typliste auszuführen.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
namespace adecc { template <typename... Types> struct defined_type_list { using type_list = std::tuple<Types...>; using type_variant = std::variant<Types...>; template <template<class...> class v_ty> using apply = v_ty<Types...>; template <class func_ty> static decltype(auto) invoke(func_ty&& f) requires requires(func_ty&& g) { std::forward<func_ty>(g).template operator()<Types...>(); } { return std::forward<func_ty>(f).template operator()<Types...>(); } }; } |
Nun können wir mit Hilfe eines Fold- Ausdrucks prüfen, ob ein bestimmter Typ ty in einer Typliste enthalten ist.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace adecc { template <class ty> using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<ty>>; template <class ty, class ty_list> struct is_in_type_list; template <class ty, class... Ts> struct is_in_type_list<ty, defined_type_list<Ts...>> : std::bool_constant<(std::same_as<remove_cvref_t<ty>, Ts> || ...)> {}; template <class ty, class ty_list> inline constexpr bool is_in_type_list_v = is_in_type_list<ty, ty_list>::value; } |
Nun kann ich diese Konzepte nutzen, um eine Liste mit gültigen Typen für die Rückgabe einer Datenbank- Abfrage zu definieren. Diese kann ich wieder zur Übersetzungszeit prüfen, und auf dieser auch ein concept definieren, um es im Programm zu nutzen. Dabei sind nicht nur Typen aus der Liste selber, sondern auch std::optional Typen mit diesen erlaubt.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
namespace adecc { template <typename ty, typename U> concept value_or_optional = std::same_as<ty, U> || std::same_as<ty, std::optional<U>>; template <class ty> struct optional_value_type { using type = void; }; template <class ty> struct optional_value_type<std::optional<ty>> { using type = ty; }; template <class ty> using optional_value_type_t = typename optional_value_type<remove_cvref_t<ty>>::type; using date_ty = std::chrono::year_month_day; using timestamp_ty = std::chrono::system_clock::time_point; using time_ty = std::chrono::hh_mm_ss<std::chrono::seconds>; using src_loc = std::source_location; using defined_values_types = defined_type_list<std::string, double, int, long long, bool, unsigned int, unsigned long long, date_ty, timestamp_ty, time_ty>; template <class... Args> struct all_result_args_ok : std::bool_constant< ( ... && ( is_in_type_list_v<Args, defined_values_types> || ( is_optional_v<Args> && is_in_type_list_v<optional_value_type_t<Args>, defined_values_types> ) ) )> {}; template <class... Args> inline constexpr bool all_result_args_ok_v = all_result_args_ok<Args...>::value; /// single value type of a defined type or optional of this template <class ty> concept db_result_type = is_in_type_list_v<ty, defined_values_types> || ( is_optional_v<ty> && is_in_type_list_v<optional_value_type_t<ty>, defined_values_types> ); /// row of value typed of the result set (e.g.. std::tuple<int, std::optional<std::string>, adecc::date_ty>) template <class ty> concept db_result_tuple = requires { typename remove_cvref_t<ty>; } && []<class... Es>(std::tuple<Es...>*) { return all_result_args_ok_v<Es...>; }( static_cast<typename std::add_pointer_t<remove_cvref_t<ty>>>(nullptr) ); |
Diese Konzepte können wir jetzt zur Datenbankabfrage benutzen, dabei ist es möglich einen einzelnen Wert oder die komplette Zeile abzurufen. Im ersten Schritt greifen wir auf ein bestimmtes Attribut in der Datenzeile zu. Da die Typen in auch als std::optional angegeben werden können, muss dieses beim Rückgabetyp berücksichtigt werden.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template <typename ty> requires db_result_type<ty> std::conditional_t<is_optional_v<ty>, ty, std::optional<ty>> Get(std::string const& strField, bool needed = true) const { if constexpr (is_optional_v<ty>) { using base_T = optional_value_type_t<ty>; auto aOpt = this->base_ty::template GetField<base_T>(strField); return aOpt; // ok: optional<base_T> converted to ty (= optional<base_T>) } else { auto aOpt = this->base_ty::template GetField<ty>(strField); if (!aOpt && needed) { throw db_type_error{"NULL encountered for non-optional result field"}; } return aOpt; // std::optional<ty> } } |
Nun können wir auch eine vollständige Datenzeile mit Hilfe von std::generator und einer Koroutine zurückgeben, allerdings bbenötigen wir dafür ein wenig Vorbereitung, mit der wir das variadic Parameter- Pack in eine sequentielle Folge zerlegen, um jeden Typeintrag zu berücksichtigen. Um nicht immer die vollständige Ergebnismenge zurückzugeben, sondern Projektionen zu ermöglichen, wird ein std::vectorstd::string mit den Namen der Attribute übergeben.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
template <typename ty> ty GetField(std::string const& strField) const { if constexpr (is_optional_v<ty>) { using Base = optional_value_type_t<ty>; return Get<Base>(strField, true); } else { auto aOpt = Get<ty>(strField, false); if (!aOpt) { throw db_type_error{"NULL encountered for non-optional result field"}; } return *aOpt; } } template <typename... Args, std::size_t... Is> std::tuple<Args...> MakeRowTupleImpl(std::vector<std::string> const& vecNames, std::index_sequence<Is...>) const { if (vecNames.size() != sizeof...(Args)) { throw std::runtime_error{"column count mismatch vs. result Args"}; } return std::tuple<Args...>{ GetField<Args>(vecNames[Is])... }; } template <typename... Args> std::tuple<Args...> MakeRowTuple(std::vector<std::string> const& vecNames) const { return MakeRowTupleImpl<Args...>(vecNames, std::make_index_sequence<sizeof...(Args)>{}); } template <typename... Args> requires (db_result_type<Args> && ...) std::generator<std::tuple<Args...>> Get() const { auto it = const_cast<logical_query*>(this)->Execute().begin(); auto aAttrs = GetAttributes(); for (; it != it.end(); ++it) { auto aTup = MakeRowTuple<Args...>(aAttrs); co_yield aTup; } co_return; } |
Da der C++Builder 13 noch keinen std::generator liefert, ist hier eine eigene Implementierung als Workaround notwendig.
Viele Entwickler und Entwicklerinnen werden in den bisherigen Beispielen nicht mehr C++ sehen und beim Lesen verzweifeln. Diese werden sicher argumentieren, dass dieses alles viel zu kompliziert ist, man es nicht benötigt. Dabei wird leicht übersehen, dass dieser Code für diese Bibliothek nur einmal geschrieben und typsicher und stabil immer wieder verwendet wird. Dabei handelt es sich um Blaupausen, die in konkreten Projekten von dem modernen C++ Compiler zusammengesetzt und geprüft werden, um dann zu effizienten, nativen Code ohne Overhead übersetzt werden. Später folgt noch ein Beispiel, dass das Befüllen einer Tabelle mit Werten aus der Datenbank zeigen wird.
Das Denken in ranges, tuples und Relationen
Das neue C++ betrachtet Daten nicht mehr einfach nur als Objekte, sondern als Beziehungen. Eine Datei, eine Datenbankquery, eine Grid-Anzeige im UI: alles sind ranges.
In meinen Streams habe ich gezeigt, wie ein Dataset aus FireDAC als range mit einer variadic Typliste definiert und als std::tuple modelliert werden kann, die mit einem Generator und Co-Routinen verwaltet und über einen Standardalgorithmus von C++ in einem Grid typsicher mit der gleichen Typliste visualisiert werden kann.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
try { // output in a vcl TStatusBar via std::println and std::clog std::println(std::clog, "test for the new range based database abstraction"); // definition alls captions for a vcl TListView static adecc::vecCaptions<adecc::AnsiStreamPolicy> captions = { { "ID", 60, adecc::EAlignmentType::right }, { "Name", 380, adecc::EAlignmentType::left }, { "Birthday", 180, adecc::EAlignmentType::center }, { "Age", 100, adecc::EAlignmentType::right } }; // concept based backend for table like operations, in this case a vcl TListView using Backend = adecc::delphi::vcl::ListViewBackend; // c++ implementation which transform a table like backend to a C++ random access iterator and view using Model = adecc::grid::GridModel<Backend, true>; // owned / borrowed // binding the concret listview (the variable lvGrid) to the model Model grid { new Backend( lvGrid ), captions, /*clear*/true }; // transaction handling in the scope, automatic rollback without explicit commit auto tx = Processes::person_db.Transaction(); // Statement with named parameter; lazy as generator<std::tuple<int, std::string, date_ty, std::optional<timestamp_ty>>> std::string strSQL = "SELECT ID, FullName, BirthDay, Age FROM Person WHERE FormOfAddress = :gender"; // typelist for the following actions using person_row = adecc::defined_type_list<int, std::string, adecc::date_ty, int>; person_row::invoke([&]<class... Ts>() { // a output_range on the grid abstaction auto sink = grid.sink<Ts...>(); // execute the query at the database connection and return a lazy input_range with the query and parameters // copy the result range into the grids output range std::ranges::copy(Processes::person_db.Execute<Ts...>(strSQL, {{ "gender", 1, true }} ), sink); }); // terminating the transaction successful tx.Commit(); } catch(std::exception const& ex) { // error message in a vcl TMemo Component std::println(std::cerr, "error while filling the grid:n{}", ex.what()); } |
Jede Zelle ist ein Tupel-Element, jede Spalte eine Projektion. Mit structure binding und Return-Value-Optimierung können Funktionen heute mehrere Werte zurückgeben, ohne Kopien zu erzeugen. Die Bedeutung von Rvalues und std::move liegt genau hier: Daten werden nicht kopiert, sondern übertragen.
Diese Effizienz ist keine Optimierung, sie ist eine Ausdrucksform. C+++ kennt die Bewegung als semantischen Begriff – und macht sie sichtbar. Wo andere Sprachen kopieren, transformiert C++ Zustände. So entsteht aus Effizienz eine Philosophie.
Diese Sichtweise verbindet Informatik und Mathematik. Ein range ist eine Menge, ein tuple ein Element, eine Transformation eine Abbildung. So wird C++ zum Werkzeug des mathematischen Denkens in Beziehungen, nicht in Anweisungen. Und damit erreicht C++ auch eine neue Stufe von Sicherheit, und die von Bjarne Stroustup definierte Notwendigkeit der strengen Typsisierung als Grundlage bekommt eine neue Qualität.
Sichere Ausgaben: formatierte Präzision
Diese neue Generation von Typsicherheit und Compile-Time-Validierung endet nicht bei den Daten oder Berechnungen, sie setzt sich konsequent bis zur Ausgabe fort.
Mit std::format hat C++ ein modernes, mächtiges und zugleich sicheres Formatierungssystem erhalten, das den klassischen, fehleranfälligen printf-Mechanismen ein Ende bereitet. std::format ist nicht nur komfortabel, sondern vollständig typsicher: Der Compiler überprüft, ob Platzhalter und Datentypen übereinstimmen.
Darüber hinaus lässt sich die Formatierung mit std::formatter erweitern: Eigene Typen können spezialisierte Formatierer besitzen, die definieren, wie ein Objekt ausgegeben wird: präzise, kontextabhängig und compile-time überprüft. Damit verschmelzen Ausdruck und Struktur zu einem einheitlichen System.
C++23 führt diese Idee weiter: Mit std::print und std::println wird das Formatieren direkt zur sicheren Ausgabe. Diese Funktionen sind compile-time optimiert, verwenden das std::format-System intern und bieten gleichzeitig eine semantisch klare, ausdrucksstarke Schnittstelle.
Ausgabe ist damit nicht länger ein unkontrollierter Seiteneffekt, sondern Teil der Typlogik. Die Formatierung wird, wie jede andere Operation in modernem C++, zum überprüfbaren Bestandteil des Programms. Sicherheit, Präzision und Lesbarkeit vereinen sich in einer der ältesten, aber immer wieder unterschätzten Dimensionen des
Softwareentwurfs: dem Ausdruck.
Verantwortung und Perspektive
C++ ist keine Sprache für Nachahmer, sondern für Entwickler und Entwicklerinnen, die verstehen, was sie bauen. Doch diese neue Phase verlangt Mut: sowohl von den Entwicklern, die Bibliotheken entwerfen, als auch von den Herstellern der Werkzeuge, die diese Sprache umsetzen.
Embarcadero hat mit dem C++Builder 13 einen wichtigen Schritt getan, um modernes C++ produktiv zu machen. Aber dieser Schritt darf nicht das Ziel sein, sondern ein wichtiger Zwischenschritt in einem fortlaufenden Prozess für die nächsten Versionen und Jahre. C++26 steht vor der Tür, und wer heute stehenbleibt, wird morgen überholt.
Der Appell geht an die Verantwortlichen: Nicht verharren, sondern weitergehen. Compiler, Frameworks und IDE müssen so entwickelt werden, dass sie das Denken in concepts, Typlisten, ranges und RAII nativ unterstützen. C++Builder hat das Potenzial, ein Teil dieser Bewegung zu sein, wenn man es zulässt.
Schlussgedanke: C++ verstehen
C++ wird oft als komplex, schwer zu lernen und unsicher bezeichnet. Dieser Ruf ist nicht gerechtfertigt. Die Sprache selbst ist nicht unsicher. Im Gegenteil, sie ist präzise, ehrlich und konsequent. Unsicher ist der Umgang mit ihr, wenn man sie nicht versteht oder in alten Mustern verharrt.
C++ bewahrt bewusst das Alte, um die Evolution nicht zu unterbrechen, aber sie erweitert sich in Schichten von Präzision, Kontrolle und Ausdruckskraft. Die Unsicherheit liegt nicht in der Sprache, sondern in der Weigerung, Neues zu lernen.
Wenn wir Bibliotheken nicht mehr als Implementierungen sehen, sondern als Modelle von Beziehungen und Zuständen, wenn wir Compiler als Partner begreifen und RAII als Prinzip der Verantwortung, wenn wir ranges als mathematische Strukturen denken, concepts als semantische Verträge nutzen, move-Semantik als bewusste Bewegung von Bedeutung verstehen und schließlich mit std::format und std::print die Sprache selbst zu einem Instrument präziser Kommunikation machen, dann beginnt eine neue Epoche des C++-Denkens.
C++ war nie eine Sprache für Bequemlichkeit, es war immer eine Sprache für Verantwortung. Und genau darin liegt ihre Zukunft.
Über den Autor
Volker Hillmann stammt aus Norddeutschland und ist Mathematiker sowie Softwarearchitekt mit einem interdisziplinären Hintergrund zwischen formaler Wissenschaft und angewandter Informatik. Sein Mathematikstudium verbindet klassische Strenge mit logischem und kybernetischem Denken, ergänzt durch eine langjährige Beschäftigung mit Chaosmathematik, Systemtheorie und Künstlicher Intelligenz. Der Informatikschwerpunkt seiner Arbeit liegt auf Datenbanken, Datensicherheit und Softwarearchitektur – mit einer konsequent modernen Ausrichtung auf C++.
Seit 1988 programmiert er in Turbo C, seit 1991 in Turbo C++, und kennt die Entwicklung der Sprache wie auch der Werkzeuge aus erster Hand. Er hat zahlreiche Vorträge über C++ und Softwarearchitektur gehalten und ist seit 2001 selbständig tätig. Seit Mitte der 2000er-Jahre ist er außerdem Embarcadero MVP und engagiert sich besonders für die Weiterentwicklung und praktische Anwendung des C++Builder.
Seine Leidenschaft gilt modernem C++:
In seinen kostenlosen Livestreams beschäftigt er sich ausschließlich mit aktuellem C++, unabhängig vom Compiler – sei es MSVC, GCC oder dank der neuen Version auch wieder der C++Builder. Dabei geht es nie um ein bestimmtes Werkzeug, sondern immer um die Sprache selbst: C++ als Ausdruck von Architektur, Präzision und Denken.
Er versteht C++ nicht nur als Werkzeug, sondern auch als Sprache des Denkens. C++ ist eine Plattform für strukturiertes, effizientes und sicheres Entwerfen. In seinen aktuellen Streams und Artikeln testet und analysiert er den C++Builder 13, um zu zeigen, wo modernes C++ in der Praxis steht, welche Möglichkeiten bereits bestehen und welche Grenzen es noch zu überschreiten gilt.
Seine Themen reichen von RAII und Move-Semantik über Coroutinen, Ranges und Concepts bis hin zu Compile-Time-Metaprogrammierung und Typsicherheit. Als Mathematiker denkt er in Systemen und Relationen, in ranges, tuples und Abbildungen, und überträgt diese Sichtweise konsequent auf Softwarearchitektur. Er steht für ein Verständnis von C++, das Verantwortung, Präzision und Evolution miteinander verbindet, und zeigt, dass diese Sprache weder veraltet noch unsicher ist, sondern die präziseste und ehrlichste Form des Softwareentwurfs.
Reduce development time and get to market faster with RAD Studio, Delphi, or C++Builder.
Design. Code. Compile. Deploy.
Free Delphi Community Edition Free C++Builder Community Edition







