D im Vergleich mit C


Inhalt <- Einleitung <- D im Vergleich mit C -> Neuerungen gegenüber C -> Zusammenfassung -> Literaturhinweise

Datentypen

D bietet eine Reihe von vordefinierten Datentypen und im Vergleich zu C auch einige Neue. In der Folgenden Tabelle sind die Typen mit ihrem C-Pendant, einer Beschreibung und Speicherverbrauch, ihrem Initialwert und ihrem Wertebereich aufgelistet.
Zu beachten ist, dass D wie C ähnlich stark typisiert ist. Die impliziten und expliziten Konversionen sind weiter unten beschrieben.

Datentyp C-Äquivalent Beschreibung und Speicherverbrauch Initialisierungswert Wertebereich
bool - Boolesche Werte false true, false
byte signed char Vorzeichenbehaftet, 8 bit 0 -128 ... 127
ubyte unsigned char Vorzeichenlos, 8 bit 0 0 ... 255
short short Vorzeichenbehaftet, 16 bit 0 -32.768 ... 32.767
ushort unsigned short Vorzeichenlos, 16 bit 0 0 ... 65.535
int int Vorzeichenbehaftet, 32 bit 0 -2.147.483.648 ... 2.147.483.647
uint unsigned int Vorzeichenlos, 32 bit 0 0 ... 4.294.967.295
long long long Vorzeichenbehaftet, 64 bit 0L -9.223.372.036.854.775.808 ...
9.223.372.036.854.775.807
ulong unsigned long long Vorzeichenlos, 64 bit 0L 0 ... 18.446.744.073.709.551.615
cent - Vorzeichenbehaftet, 128 bit (derzeit noch nicht aktiv) 0 -170.141.183.460.469.231.731.687.303.715.880.000.000 ...
170.141.183.460.469.231.731.687.303.715.880.000.099
ucent - Vorzeichenbehaftet, 128 bits (derzeit noch nicht aktiv) 0 0 ... 340.282.366.920.938.463.463.374.607.431.770.000.000
float float Fließkommazahl, 32 bit float.nan 1.17549e-38 ... 3.40282e+38
double double Fließkommazahl, 64 bit double.nan 2.22507e-308 ... 1.79769e+308
real long double größte mögliche Zahl, die Hardware bereitstellen kann
(z.B. 80 bit Größe für x86 CPUs)
real.nan Hardwareabhängig
ifloat float _Imaginary Imaginäre Fließkommazahl float.nan * 1.0i Wie float
idouble double _Imaginary Imaginäre Fließkommazahl double.nan * 1.0i Wie double
ireal long double _Imaginary Imaginäre real real.nan * 1.0i Hardwareabhängig
cfloat float _Complex Komplexes float float.nan + float.nan * 1.0i 1.17549e-38+1.17549e-38i ...
3.40282e+38+3.40282e+38i
cdouble double _Complex Komplexes double double.nan + double.nan * 1.0i 2.22507e-308+2.22507e-308i ...
1.79769e+308+1.79769e+308i
creal long double _Complex Komplexes real real.nan + real.nan * 1.0i Hardwareabhängig
char char Vorzeichenlos, 8 bit UTF-8 0xFF UTF-8 Zeichensatz
wchar wchar_t (sizeof(wchar_t) = 2) Vorzeichenlos, 16 bit UTF-16 0xFFFF UTF-16 Zeichensatz
dchar wchar_t (sizeof(wchar_t) = 4) Vorzeichenlos, 32 bit UTF-32 0x0000FFFF UTF-32 Zeichensatz

In D weisen die Datentypen zusätzlich noch spezielle Eigenschaften auf, die abgerufen werden können. Eigenschaften, die für alle Datentypen gleichermaßen gelten, sind init, sizeof, alignof und magleof. Diese beschreiben den Initialisierungswert, die Größe, die Speicherausrichtung und den vom Compiler intern zugeteilten Namen des Datentyps.
Implizit lassen sich alle Ganzzahldatentypen gegenseitig, als auch zu Fließkomma- und Zeichendatentypen konvertieren. Fließkommatypen lassen sich nur gegenseitig implizit konvertieren, und Zeichentypen lassen sich implizit gegenseitig, als auch zu Fließkomma- und Ganzzahldatentypen konvertieren. Jede andere Konversion muss explizit, entweder über die cast(Datentyp)-Funktion oder über eine Umwandlung, beispielweise wenn gerundet werden soll, passieren. Zu beachten ist, dass das Inkrementieren über Zeigerarithmetik und anschließende derefenzieren eines Arrays wie es in C möglich war nur noch über einen expliziten Cast möglich ist, da der Compiler zwischen Array und Zeiger trennt.

Kommentare

Kommentare lassen sich in D auf verschiedene Arten ausdrücken. D kann über Compileroperationen automatisch eine Dokumentation erzeugen. Dadurch muss man zwischen einfachen und Dokumentationskommentaren unterscheiden.
// Ein einzeiliger Kommentar
/*
  Ein mehrzeiliger Kommentar
*/
/+
// Ermöglicht verschachtelte Kommentare
+/
/// Ein einzeiliger Dokumentationskommentar
/**
*  Ein mehrzeiliger Kommentar, der in die Dokumentation eingeht
*/
/++
+ Auch dies ist möglich
+/

struct und union

Wie auch in C werden von D die strukturierten Datentypen struct und union bereitgestellt. Der folgende Code erläutert die Deklaration eigener Strukturen. Auffällig ist, dass in D kein abschließendes Semikolon mehr nötig ist.
union U { 
   int a; 
   double b; 
}

struct X { 
   int a; 
   int b; 
   int c; 
   int d = 7;
}		

Arrays

Die Arrays in D sind ein zentraler Punkt, was die Einfachheit der Sprache betrifft. Während man in C für dynamischen Arrays aufwändige Speicherverwaltung betreiben muss, lässt sich dies in D mit dem in der Sprache integrierten, dynamischen Array auf leichte Art und Weise lösen. Auch das Konkaterieren von Strings, die in D ein Array aus Character sind, wird so leichter umsetzbar, als dies in C der Fall war.
Dynamische Arrays können in D mit " ~ " einfach konkateniert werden, sofern sie die gleichen Typen beinhalten. Die Beispiele stellen dar, wie einfach vor allem die Stringkonkatenation dadurch wird.
Neben den dynamischen Arrays gibt es in D assoziative Arrays. Diese sind in Java als Maps bekannt und ermöglichen die Kombination aus Schlüssel-Wert-Paaren. Der Indextyp muss dabei kein Ganzzahldatentyp sein, auch Strings sind nutzbar.
Arrays lassen sich in D auf leichte Weise slicen und kopieren, die Codebeispiele machen die Syntax deutlich. Es ist darauf zu achten, dass beim Slicen nur Referenzen auf Elemente des Ursprungsarrays im Zielarray erzeugt werden, womit eine Änderung eines Elementes im Zielarray sich direkt auf das Ursprungsarray auswirkt. Auch werden bei der Bereichsangabe 1..3 nicht die Elemente 1, 2 und 3 genommen, sondern nur die Elemente 1 und 2.
Zahlreiche Eigenschaften sind über Arrays erfragbar und lassen sich auch setzen. So kann man mit .length, .reverse, .sort, .dup, .keys, .values die Länge erfragen, die Elemente in ihrer Reihenfolge umkehren, die Elemente sortieren (sofern eine totale Ordnung für den Datentyp definiert ist), alle Schlüssel und alle Werte zurückliefern. Über das Setzen einer Eigenschaft wie der Länge lässt sich ein Array sogar verkleinern oder vergrößern. Dies wirkt sich natürlich gleich auf vielfache Art auf viele Bereiche im Code aus. Beispielsweise muss man keinen Zähler mehr mitführen, der die Anzahl der Elemente im Array speichert, somit braucht man auch kein argc mehr, wie es in C üblich war. argv[] und argc sind in D zu args verschmolzen. Braucht man beispielsweise die Anzahl der Kommandozeilenparameter, ist dies über args.length auf einfache Art und Weise möglich.
// Stringkonkatenation in D für "Hallo Welt!"
char[] string = "Hallo";
string ~= " Welt!";

// Assoziative Arrays
int[char[]] groessen;
groessen["Nowitzki"] = 213;
groessen["Lahm"] = 170;
writefln("Philipp Lahm ist %d gross.", groessen["Lahm"]);

// Slicen eines Arrays
int[] b;
b = a[1..3]; 
// Im Array b sind nun bei 0 und 1 die Referenzen
// auf die Elemente 1 und 2 im Array a enthalten

// Kopieren eines Arrays bei bekannter Länge
int[5] b;
b[] = a[];

// Kopieren eines Arrays bei unbekannter Länge int[] b; b = a.dup;
// Hello World! in D
import std.stdio;

int main(char[][] args) {
  writefln("Hello World!");
  return 0;
}

Funktionen

Während Referenzparameter in C nur über Zeiger möglich waren, gibt es für diesen Anwendungsfall in D neue Sprachkonstrukte. Die Schlüsselwörter in, inout und out definieren Eingabeparameter, Ein- und Ausgabeparameter und Ausgabeparameter. Der Standard ist dabei das Schlüsselwort in und muss demnach nicht extra angegeben werden. Allgemein übertragen bezeichnet man in auch als Call-by-Value und inout und out als Call-by-Reference. Der Anwendungsfall, bei dem eine Funktion nur einen Ausgabeparameter erzeugt, von welchem er aber nicht abhängig ist (out), ist zum Beispiel denkbar, wenn eine Funktion true oder false als erfolgreiche oder nicht erfolgreiche Durchführung zurückgeben soll, aber andererseits noch ein Ergebnis berechnet, welches nicht mit über den return-value zurückgegeben werden kann. Es ist dabei zu beachten, dass ein reiner Ausgabeparameter mit dem Standardwert des Datentyps innerhalb der Funktion zur Verfügung steht.
In D ist es außerdem möglich, durch eine Änderung des Funktionskopfes eine Funktion zu überladen. Somit lässt sich unterschiedliches Verhalten bei gleichem Funktionsnamen durch die Änderung der Parameter realisieren.
Um in C eine Funktion zu benutzen, die erst weiter unten im Quellcode deklariert wird, ist es entweder nötig, den Funktionskopf in eine eingebundene Headerdatei zu schreiben oder die Funktion bereits vorher zu deklarieren/implementieren. In D ist es nun möglich, Funktionen an jeder Stelle des Quellcodes zu deklarieren, und sie dabei von überall abrufbar zu haben.
Die folgenden Codebeispiele sollen die Anwendungszwecke und Syntax der Übergabearten erläutern.
// Standard: Call-by-Value (in)
int add(int i) {
  i++;
  return i;
}

// Ueberladen
float add(float i) {
  i--;
  return i;
}

// Referenzparameter
void add_rf(inout int i) {
  i++;
}

// Referenzparameter als Zeiger
void add_rfp(int * i) {
  (*i)++;
}

// Ausgabeparameter
void add_out(int i, out int j) {
  i++;
  j = i;
}

Zeiger

Die Zeigerprogrammierung aus C ist auch in D weiterhin möglich. Die leider noch wenig vorhandenen, in der Sprache implementierten Datenstrukturen (z.Zt. dynamische Arrays und assoziative Arrays) machen die Entwicklung eigener Strukturen wie Listen, Bäume, etc. nötig, so dass man nicht an Zeigerprogrammierung vorbeikommt.
Ein weiterer, denkbarer Anwendungsfall für die Übergabe von Referenzen und Zeigern ist die Nutzung alter C-Funktionen, die sich bewährt haben und in D weiter verwendet werden sollen. sscanf() ist dafür ein Beispiel. Auch malloc(), realloc() und free() werden unter anderem aus den C-Bibliotheken aufgerufen.
In D gibt es keine sizeof()-Funktion mehr. Die Größe eines Datentyps kann über dessen Eigenschaft erfragt werden.
Wie bereits angedeutet ist das Durchlaufen eines Arrays über Zeigerarithmetik und Derefenzieren in D nur noch über einen expliziten Cast möglich:
// Zeigerarithmetik mit Arrays in C
for (j = 0; j < length; j++) {
  fprintf(stdout, "%d ", *a+j);
}

// Zeigerarithmetik mit Arrays in D
for (j = 0; j < a.length; j++) {
  writefln(stdout, "%d ", *(cast(int*)a+j));
}		

Funktionszeiger / Delegates

Die aus C bekannten Funktionszeiger haben in D eine andere Syntax erhalten, haben sich vom Prinzip her aber nicht geändert. Sie ermöglichen es, einer Variablen eine Referenz auf eine Funktion zuzuweisen, dessen Rückgabewert dem Datentyp der Variablen entspricht. Dies ermöglicht hohe Flexibilität zur Laufzeit des Programms, da Funktionen somit auch als Parameter verwendet werden können und somit das Verhalten des Programmes direkt zur Laufzeit gesteuert werden kann. Der Funktionszeiger aus C ("type(*)(Parameter)") ist in D nun "type function(Parameter)" und ähnelt somit syntaktisch dem Delegate.
In D lassen sich über Delegates auch Methodenzeiger implementieren, diese Zeiger realisieren die Referenz auf eine Methode einer Klasse. Während Funktionszeiger nur auf statische Funktionen zeigen können, erlaubt es der Delegate, auch auf nicht-statische Funktionen/Methoden zu zeigen. In der nächsten Entwicklungsstufe soll es möglich sein, mit Delegates auch sogenannte Closures zu realisieren. Closures sind vor allem in funktionalen Programmiersprachen ein häufig anzutreffendes Konzept und halten in imperativen Programmiersprachen immer mehr Einzug. Closures werden auch als "Funktionsabschlüsse" bezeichnet und stellen Werte auch außerhalb ihres Kontextes zur Verfügung.
// Klassendefinition
private class Trigonometrie {
  public real sinus (real i) {
    return sin(i);
  }
}

// Deklaration des Delegate
real delegate(real) dg;

// Instanz der Klasse bilden
Trigonometrie t = new Trigonometrie();

// Zuweisen der Methode
dg = &t.cosinus;

Performance

Ein großer Punkt bei der Entwicklung der Sprache war es, die gleiche Effizienz wie ein C/C++ Programm zu erreichen. Dies soll vor allem dadurch ermöglicht werden, dass sich die hardwarenahe Programmierung ebenfalls in D umsetzen lässt, andererseits allerdings auch auf erhöhter Abstraktionsebene agiert werden kann, wenn Geschwindigkeit eine untergeordnete Rolle spielt.
Auf der einen Seite kann in D ganz normal wie in C mit Zeigern programmiert werden, auf der anderen Seite lassen sich komplexe Datenstrukturen oder Entwurfsmuster in objektorientierter Weise flexibel gestalten. Aber das ist noch nicht genug, D bietet einen inline-Assembler, der eine noch maschinennähere Programmierung ermöglicht, als es mit reinem C-Code möglich wäre.
Experimente mit C++-, Java-, D- und Lisp-Entwicklern haben gezeigt, dass Entwicklungsaufwand und Performance von D-Programmen sehr konkurrenzfähig sind und in vielen Disziplinen ist D sogar Spitzenreiter. Während Lisp bei einem Programmierwettbewerb, bei dem ein Nummernverzeichnis verarbeitet werden sollte, deutlicher Sieger über Java und C++ in Sachen Entwicklungszeit und Zeilenanzahl war, konnte ein erfahrener C++-Entwickler das gleiche Programm in deutlich kürzerer Zeit in D umsetzen. Interessant ist bei diesem Versuch die Tatsache, dass der durchschnittliche Lisp-Entwickler deutlich mehr Programmiererfahrung aufweisen kann als ein D-Entwickler und trotzdem unterlegen ist. Es stellt sich somit heraus, dass zumindest für gewisse Anwendungsbereiche D sicherlich eine ernstzunehmende Alternative ist, was Entwicklungszeit von Anwendungen betrifft. Ein Verweis auf diesen Versuch und eine ausführlichere Erläuterung ist in den Literaturhinweisen zu finden.
Auf der anderen Seite lassen sich gleiche Funktionalitäten in D gegenüber C++ deutlich effizienter realisieren. Ein weiterer Versuch hat gezeigt, dass ein wordcount-Programm, welches jedes Wort in einem Text und seine Vorkommen zählt, in D sehr effizient implementiert, compiliert und ausgeführt werden kann. Ein entsprechendes C++-Programm war bei diesem Versuch in der Ausführung mindestens doppelt, in einigen Fällen sogar drei mal so langsam. Auch das Übersetzen der Programme geschah unter D deutlich schneller. In den Literaturhinweisen sind Verweise auf diese Seiten hinterlegt. Reinzuschauen lohnt sich auf alle Fälle.

Präprozessor

Der in C vielfach genutzte Präprozessor wurde in D abgeschafft. Für Konstanten gibt es nun das Schlüsselwort const, über welches sich eine Konstante definieren lässt und somit ein Makro überflüssig macht.
Einzubindende Quellen werden nun über import (ohne #, da es ja keinen Präprozessor gibt) eingebunden, somit übernimmt der Compiler das Zusammenfügen mehrerer Quellen.
Ein weiterer wichtiger Einsatzpunkt des C-Präprozessors ist das bedingte compilieren. Bedingtes Compilieren lässt sich auf andere Art und Weise in D umsetzen. Der Code lässt sich zum Beispiel auf verschiedene Module aufteilen, die je nach Version mit dem version-Statement und dem debug-Statement unterschiedlich compiliert werden. Der Compiler bietet diesbezüglich weitere Optionen.


Inhalt <- Einleitung <- D im Vergleich mit C -> Neuerungen gegenüber C -> Zusammenfassung -> Literaturhinweise