Code Arts

D: C++ Referenzen – Keyword für NotNull Referenzen

Posted in:

Wie viele wissen, sind Java, C# und D, im Gegensatz zu C++, Sprachen deren Objekte immer per Referenz übergeben werden.
Doch was genau sind Referenzen?
Hier hilft die C++ Beschreibung (in anderen Sprachen ist es intern wohl genauso, jedenfalls wäre mir nichts gegenteiliges bekannt):

Referenzen sind interne Zeiger auf Variablen. Sie werden also genau so verwendet wie gewöhnliche Variablen, verweisen jedoch auf das Objekt, mit dem sie initialisiert wurden. Die Zeigerverwendung wird vor dem Programmierer verborgen.

(Quelle)

In Kurzform: es sind sich selbst dereferenzierende Zeiger. Mit der C untypischen Sicherheit, dass das was sich dahinter verbirgt garantiert nicht NULL ist.

Dagegen haben Java, C# und D den Vorteil, dass man bspw. nicht extra darauf achten muss, bei Übergabe an eine Funktion/Methode, den/die Parameter mit “const Object& obj” zu kennzeichnen.
In den oben genannten 3 Sprachen ist es einfach und intuitiv: “Object obj” oder “const Object obj” wenn es eben konstant sein soll/muss.
Doch dies hat auch einen Nachteil. Wenn man sich die Dokumentation zu C++ Referenzen durch liest springt ein besonderer und, wie ich finde, wichtiger Punkt in der Vordergrund:
C++ Referenzen können nicht “NULL” (ein C Makro, ein Relikt woraus Java, C+ und D das Keyword “null” und C++11 das Keyword “nullptr” formten) sein, im Gegensatz zu Objekten bzw. Referenzen in Java, C# und D. In C++ müssen Referenzen mit der Instanz bzw. der Referenz der Instanz eines Objektes belegt sein. Und danach kann ihr auch kein anderes Objekt zugewiesen werden.
In den anderen dreien ist das alles allerdings sehr gut möglich und auch gewollt.

So ist dies in C#, Java und D zulässig:

Foo tf = new Foo();
tf = new Bar(); // Bar ist eine Kind-Klasse von Foo
 
// oder
 
Foo tf2 = new Foo();
Bar tb = new Bar(); // Bar ist eine Kind-Klasse von Foo
 
tf2 = tb;

Genau wie:

void foo(Foo f = null) { }

Auf den ersten Blick mag das vielleicht vorteilhaft klingen, so das man default Objekt Parameter einfach mit null belegen kann, wie man es evtl aus Java und C# auch kennt. Ja das ist schön. Aber wieso dieser Weg, wenn es auch Pointer gibt?

void foo(Foo* f = null) { }

Wenn eine Sprache beides anbietet, warum dann erlauben, dass normale Referenzen null sein können?
In C++ wurden Referenzen eingeführt, da Pointer, wie man sie aus C kennt, auch NULL sein konnten/durften.
Also warum ist in D für beide null erlaubt? Pointer können null sein, das empfinde ich als logisch, aber warum das auch für normale Referenz-Variablen gilt, ist mir bis heute nicht so ganz klar. Allerdings ist es so auch in C# und Java, wenn man von den Punkt absieht, dass es in Java keine Pointer gibt und in C# diese nur in “unsafe” Blöcken erlaubt sind.

Meine Aufgabe bestand für mich nun darin, das aus C++ bekannte Verhalten nachzubilden, vor allem, um nicht immer wegen einer nichts sagenden “Access Violation” zu debuggen und zu schauen, wo ich denn auf leeren Speicher versuche zuzugreifen. Nervige Angelegenheit.

Am Anfang klang es recht leicht. Ein alias this ConvertToRef; in der zu konvertierenden Klasse + die zugehörige Methode, sowie in der Referenz Struktur/Klasse ein äquivalentes alias this this._obj; das den Zugriff auf das interne Objekt ermöglicht.
Doch leider führt dies in der momentanen Version von dmd 2.059 zu einem Stackoverflow, da der Compiler nicht klug genug ist, zu unterscheiden, wann dieser Zyklus aufhört. Er konvertiert solange hin und her bis der gesamte Stack überläuft.
Bravo. Hoffentlich wird das mitunter in dmd 2.060 gefixt.

Doch wozu gibt es opDispatch und opCast? Mit ein wenig Einfallsreichtum, der Mithilfe einiger aus dem Forum die auch für eine NotNull Referenz Semantik standen und dem aufspüren und Hack entwickeln für einige weitere Bugs, war es innerhalb weniger Tage geschafft.
Hier präsentiere ich, genau wie im Forum, die Ref bzw. NotNull Struktur:

Show »

struct Ref(T : Object) {
private:
	T _obj;
 
public:
	@disable
	this();
 
	@disable
	this(typeof(null));
 
	this(T obj, string file = __FILE__, uint line = __LINE__) {
		if (obj is null) {
			throw new Exception("1. Object \"" ~ T.stringof ~ "\" is null.", file, line);
		}
 
		this._obj = obj;
	}
 
	@disable
	typeof(this) opAssign(typeof(null)) inout;
 
	@disable
	typeof(this) opAssign(T) inout;
 
	@disable
	typeof(this) opAssign(Ref!(T)) inout;
 
	inout(T) GetAccess() inout {
		return this._obj;
	}
 
	version (none) {
		alias Get this;
	} else {
		template opDispatch(string name) {
			static if (__traits(hasMember, this._obj, name)) {
				static if (is(typeof(__traits(getMember, this._obj, name)) == function)) {
					auto opDispatch(Args...)(Args args) inout {
						return mixin("this._obj." ~ name ~ "(args)");
					}
				} else {
					void opDispatch(V)(V var) {
						mixin("this._obj." ~ name ~ " = var;");
					}
 
					/*
					 * Only for correct error message.
					 * Meaning: "can only initialize const member XY inside constructor"
					 *
					 */
					void opDispatch(V)(V var) const {
						mixin("this._obj." ~ name ~ " = var;");
					}
				}
			} else {
				void opDispatch() inout {
					throw new Exception("Member not found!");
				}
			}
		}
	}
 
	U opCast(U : Object)() {
		static if (!is(U == T)) {
			return *(cast(U*) &this._obj);
		} else {
			return this._obj;
		}
	}
 
	U opCast(U : Object)() const {
		static if (!is(U == T)) {
			return *(cast(U*) &this._obj);
		} else {
			return cast(T) this._obj;
		}
	}
 
	/// to enable const Ref!(T)
	const(typeof(this)) opCast(U = typeof(this))() const {
		return this;
	}
}

Und das zugehörige Template welches per mixin TRef!(typeof(this)); in solche Klassen, welche als Not Null Referenz übergeben werden sollen können, eingebunden werden muss:

Show »

template TRef(T) {
	final Ref!(T) ConvertToRef(string file = __FILE__, uint line = __LINE__) in {
		if (this is null) {
			throw new Exception("2. Object \"" ~ T.stringof ~ "\" is null.", file, line);
		}
	} body {
		return Ref!(T)(this);
	}
 
	final Ref!(T) ConvertToRef(string file = __FILE__, uint line = __LINE__) const in {
		if (this is null) {
			throw new Exception("3. Object \"" ~ T.stringof ~ "\" is null.", file, line);
		}
	} body {
		return Ref!(T)(*(cast(T*) &this));
	}
 
	alias ConvertToRef this;
}

Und im folgenden noch die Anwendung und einige Beispiele & Tests:

Show »

class Foo {
public:
	string test;
 
	void echo() const {
		writeln("Put together!" ~ this.test);
	}
 
	mixin TRef!(typeof(this));
}
 
class Bar : Foo {
public:
	override void echo() const {
		writeln("Putting!" ~ this.test);
	}
}
 
void a(Foo f) {
	f.echo();
}
 
void b(const Foo f) {
	f.echo();
}
 
void c(Ref!(Foo) f) {
	f.echo();
 
	Bar b1 = cast(Bar) f;
	b1.echo();
 
	const Bar b2 = cast(Bar) f;
	b2.echo();
 
	write("Original: ");
	Foo original = cast(Foo) f;
	original.echo();
}
 
void d(const Ref!(Foo) f) {
	writeln("Const: ");
 
	f.echo();
 
	Bar b1 = cast(Bar) f;
	b1.echo();
 
	Bar b2 = cast(Bar) f;
	b2.echo();
}
 
void compare(Ref!(Foo) rf, Foo f) {
	Foo of = cast(Foo) rf;
	Foo of2 = rf.GetAccess();
 
	if (of == of2) {
		writeln("Of == Of2");
	}
 
	if (of == f) {
		writeln("Of == F");
	}
 
	writefln("Ref!(Foo): %d, %d, Foo: %d, %d", rf.sizeof, rf.alignof, f.sizeof, f.alignof);
}
 
void change_ref(Ref!(Foo) rf) {
	rf.test = " -> Reference Power!";
}
 
void change_noref(Foo f) {
	f.test = " -> No Reference!";
}
 
void from(Ref!(Foo) rf) {
	//to(rf);
	to(rf.GetAccess());
}
 
void to(Foo f) {
	///from(f);
}
 
void del(Ref!(Foo) f) {
	Foo nf;
 
	//f = nf;
	//f = null;
 
	f.echo();
}
 
void main() {
	Foo f = new Foo();
 
	a(f);
	b(f);
	c(f);
	d(f);
 
	writeln("#### null ref");
 
	Foo f2;
 
	///a(f2);
	///b(f2);
	///c(f2);
	///d(f2);
 
	Bar b1 = new Bar();
 
	a(b1);
	b(b1);
	///c(b1);
 
	compare(f, f);
 
	change_ref(f);
 
	f.echo();
 
	change_noref(f);
 
	f.echo();
 
	del(f);
 
	writeln("#### const");
 
	const Foo cf = new Foo();
 
	///a(cf);
	b(cf);
	b(cf);
	c(cf);
	d(cf);
 
	writeln("#### lvalue");
 
	a(new Foo());
	b(new Foo());
	c(new Foo());
	d(new Foo());
 
	///to(new Foo());
	from(new Foo());
}

Ich hoffe es findet Anklang, würde mich über Kritik und Anregungen wie immer sehr freuen.

6 Comments

  1. GreenPepper – 10. Juni 2012 Reply

    Ach du meine Güte, ist das ein übler Hack ;) Respekt, du hast dich zum richtigen D-Experten entwicklet!

    1. Architekt – 10. Juni 2012 Reply

      Geht so ;) Denke das ist wie in C++, man wird nie ganz auslernen.
      Machst du noch was in D oder ist C# deine neue Liebe?

      1. GreenPepper – 12. Juni 2012

        Hehe,
        wie gesagt, ich hätte immer noch Interesse, was mit Dgame zu machen..

      2. Architekt – 12. Juni 2012

        Ist in Arbeit, Audio fehlt noch.

      3. GreenPepper – 12. Juni 2012

        Cool! Vllt. kannst du ja dann noch einen Blogeintrag mit den wichtigsten Änderungen und einen Quickstart machen, das würde mir einiges an Zeit ersparen ;)
        Freu mich schon auf die neue Version!

      4. Architekt – 12. Juni 2012

        Das wird keine neue Version, das wird eine komplett überarbeitete Version 1.0, da wird es keine Änderungen geben sondern alles vollkommen neu designet sein. ;)

Leave a comment