In diesem Teil des Einsteigerguides geht es direkt zur Sache: Wir produzieren den ersten Patch. Ihr werdet noch keinen großen Überblick über die Codebasis als Ganzes erhalten, weil ich glaube, dass man sich so etwas am besten erarbeitet, indem man im Code herumwühlt.
Darum geht es heute um zwei Dinge - erstens: Eine kleine (unvollständige) Einführung in Referenzen und const-correctness, und zweitens: Wie benutze ich Mercurial (unser Versionskontrollsystem, Ihr erinnert Euch), um einen Patch zu produzieren.
Wechseln wir zuerst in das Verzeichnis, in das wir den ASC Quellcode geklont haben:
Code: Alles auswählen
cd ~/code/asc/
Code: Alles auswählen
ls
Code: Alles auswählen
cd source
Code: Alles auswählen
ls
der Plan
Unser Plan ist einfach: Wir versuchen, eine Funktion zu finden, in der eine Referenz zu irgendwas übergeben wird, aber das Objekt, auf das verwiesen wird, nicht verändert wird, also konstant bleibt. Wenn es konstant bleibt, können wir den Parameter mit dem Keyword const kennzeichnen - das sagt dem Compiler: Dieser Parameter wird in der Funktion nur lesend verwendet. Wichtiger ist aber, dass es dem Programmierer kennzeichnet, wie der Parameter verwendet wird. Wenn ich also als Entwickler die Funktionssignatur sehe (als Signatur bezeichnet man die Parameter und den Rückgabetyp einer Funktion), dann weiß ich schon: Aha, dieser Parameter wird nicht verändert.
Das ist wichtig, denn wenn ich eine Funktion aufrufe, die eine nicht-const Referenz als Parameter nimmt, dann kann sie mit der Variable, die ich Ihr übergebe, irgendwas anstellen, und hinterher ist die nicht mehr die gleiche. Um sicherzugehen, muss ich mir die Implementation der Funktion anschauen, und das ist nervig und fehleranfällig. Ist der Parameter aber const, dann kann ich ihr bedenkenlos eine Variable als Parameter übergeben, ohne in die Implementation der Funktion hineinzuschauen, weil ich weiß, dass ich die Variable nach dem Funktionsaufruf genauso aussieht wie davor.
Also: Öffnen wir die Datei autotraining.cpp in unserem Editor (z.B. Geany). Schauen wir uns die Datei an: Es werden zwei Funktionen definiert. In der zweiten gibt es einen Parameter, der als nicht-const-Referenz übergeben wird: Der Paramter player. Wenn wir uns die Funktion so anschauen, dann scheint es nicht so zu sein, als würde auf den Inhalt der Referenz schreibend zugegriffen, daher ändern wir die Signatur von
Code: Alles auswählen
void automaticTrainig( GameMap* gamemap, Player& player )
Code: Alles auswählen
void automaticTrainig( GameMap* gamemap, const Player& player )
die Änderung
Logischerweise muss die Deklaration mit der Implementation übereinstimmen. Öffnen wir die Datei autotraining.h und ändern wir die Zeile
Code: Alles auswählen
extern void automaticTrainig( GameMap* gameMap, Player& player );
Code: Alles auswählen
extern void automaticTrainig( GameMap* gameMap, const Player& player );
Code: Alles auswählen
make -j
das erste Problem
Da wir zwar eine Funktion geändert haben, aber noch nicht die Stellen im Code, die diese Funktion aufrufen, werden wir auf jeden Fall mindestens eine Fehlermeldung bekommen. Und da ist sie auch schon
Code: Alles auswählen
[...]
make[3]: Entering directory `/home/christian/src/ascdefault/asc/source/unittests'
[...]
main.cpp: In function 'int runTester()':
main.cpp:140:76: error: no matching function for call to 'SigC::Signal2<void, GameMap*, Player&>::connect(SigC::Slot2<void, GameMap*, const Player&>)'
main.cpp:140:76: note: candidate is:
In file included from /usr/include/sigc++-1.2/sigc++/sigc++.h:27:0,
from ../../source/ai/../util/messaginghub.h:28,
from ../../source/ai/../typen.h:42,
from ../../source/ai/ai.h:34,
from main.cpp:6:
/usr/include/sigc++-1.2/sigc++/signal.h:723:18: note: SigC::Connection SigC::Signal2<void, P1, P2, Marsh>::connect(const InSlotType&) [with P1 = GameMap*; P2 = Player&; Marsh = SigC::Marshal<void>; SigC::Signal2<void, P1, P2, Marsh>::InSlotType = SigC::Slot2<void, GameMap*, Player&>]
/usr/include/sigc++-1.2/sigc++/signal.h:723:18: note: no known conversion for argument 1 from 'SigC::Slot2<void, GameMap*, const Player&>' to 'const InSlotType& {aka const SigC::Slot2<void, GameMap*, Player&>&}'
[...]
Offenbar geht irgendwas in der Datei unittests/main.cpp in Zeile 140 mächtig schief. Schauen wir uns mal diese Zeile an:
Code: Alles auswählen
GameMap::sigPlayerTurnEndsStatic.connect( SigC::slot( automaticTrainig ));
Signale und Slots
ASC verwendet für viele Dinge ein sogenanntes Signal- (oder Signal/Slot-) Framework namens SigC++. Es gibt auch andere solche Frameworks, aber das Prinzip dahinter ist immer gleich:
Ich definiere irgendwo ein Signal. Das Signal ist eine Art Objekt, mit dem ich Funktionen verbinden kann. Das passiert in der Zeile oben mit der Methode connect. Diese Funktionen müssen aber in ein Slot passen. Ein Slot ist nicht viel mehr als eine Funktionssignatur. Alle Funktionen, die eine passende Signatur aufweisen, können also mit diesem Signal verbunden werden. Das Signal kann dann von allen möglichen Stellen im Code getriggered werden. Alle Funktionen, die damit verbunden sind, werden dann ausgeführt.
Warum macht man das? Warum führt man nicht einfach gleich die richtigen Funktionen aus? Die Antwort ist: Der Signal-Mechanismus erlaubt, sozusagen quer zu programmieren. Angenommen, ich möchte eine Funktion immer dann ausführen, wenn eine Runde endet. Dann könnte ich entweder die Stelle im Code suchen, wo das Rundenende definiert wird. Wenn ich Pech habe, gibt es verschiedene Stellen im Code, wo ein Rundenende eingeläutet werden kann.
Praktischer ist es da, wenn ich weiß, dass zum Ereignis "Rundenende" immer das Signal "Rundenende" getriggered wird. Dann muss ich nur meine Funktion mit diesem Signal verbinden und voila - Es funzt, ohne dass ich überall in meiner Codebasis rumwühlen muss.
Zurück zu unserer Codezeile oben: Da ich die Funktionssignatur unserer Funktion geändert habe, passt sie also nicht mehr in das Slot. Ich muss nun die Slot-Definition anpassen. Wie mache ich das? Die Codezeile verrät eigentlich alles: Das Signal ist eine Eigenschaft der Klasse GameMap und heißt sigPlayerTurnEndsStatic. Also öffnen wir die Dateien gamemap.cpp und gamemap.h und verändern in gamemap.h:
Code: Alles auswählen
static SigC::Signal2<void,GameMap*,Player&> sigPlayerTurnEndsStatic;
Code: Alles auswählen
static SigC::Signal2<void,GameMap*, const Player&> sigPlayerTurnEndsStatic;
Code: Alles auswählen
SigC::Signal2<void,GameMap*,Player&> GameMap::sigPlayerTurnEndsStatic;
Code: Alles auswählen
SigC::Signal2<void,GameMap*, const Player&> GameMap::sigPlayerTurnEndsStatic;
Code: Alles auswählen
make -j
Jetzt kommt bei mir eine neue Fehlermeldung:
Code: Alles auswählen
[...]
./../../autotraining.cpp: In Funktion »void automaticTrainig(GameMap*, const Player&)«:
./../../autotraining.cpp:64:71: Fehler: Umwandlung von »std::list<Building*>::const_iterator {aka std::_List_const_iterator<Building*>}« in nicht-skalaren Typen »std::list<Building*>::iterator {aka std::_List_iterator<Building*>}« angefordert
./../../autotraining.cpp:67:68: Fehler: Umwandlung von »std::list<Vehicle*>::const_iterator {aka std::_List_const_iterator<Vehicle*>}« in nicht-skalaren Typen »std::list<Vehicle*>::iterator {aka std::_List_iterator<Vehicle*>}« angefordert
[...]
Iteratoren
Schauen wir uns die erste Zeile an, über die der Kompiler meckert (und die nächste gleich mit):
Code: Alles auswählen
for ( Player::BuildingList::iterator b = player.buildingList.begin(); b != player.buildingList.end(); ++b )
autoTrainer( *b );
Wie aber sehe ich, ob ich nicht schon durch bin mit den Gebäuden? Dafür gibt es einen speziellen Wert, den die List mit der Methode end() zurückgibt. Er deutet an: Die Liste ist zu Ende. Also überprüfe ich jedes mal, ob mein Iterator diesen Wert hat. Wenn nicht, mache ich weiter. Wenn ja, breche ich ab.
Was ist nun mit der Funtion oben falsch? Das hat offensichtlich mit dem const zu tun, das wir eingefügt haben - etwas anderes haben wir ja nicht verändert. In der Tat gibt es zwei unterschiedliche Typen von Iteratoren: Normale und konstante Iteratoren. Ein konstanter Iterator ist nicht vom Typ iterator, sondern vom Typ const_iterator. Also müssen wir in der Zeile oben den Typ des Iterators ändern:
Code: Alles auswählen
for ( Player::BuildingList::const_iterator b = player.buildingList.begin(); b != player.buildingList.end(); ++b )
autoTrainer( *b );
Code: Alles auswählen
for ( Player::VehicleList::const_iterator v= player.vehicleList.begin(); v != player.vehicleList.end(); ++v )
autoTrainer( *v );
Code: Alles auswählen
make -j
Commit
Wir wollen unsere großartige Verbesserung natürlich mit dem Rest der Welt teilen. Dazu committen wir unsere Änderung, das heißt: Wir teilen unserem Versionscontrolsystem mit, dass es Änderungen gegeben hat. Dazu geben wir eine kleine Beschreibung an, was wir geändert haben, so dass nachfolgende Generationen wissen, was in dem Commit passiert ist.
Bevor wir den Commit absetzen, richten wir Mercurial noch ein. Das ist wichtig, damit andere den Commit mit unserer Person in Verbindung bringen können. Legt dazu eine Datei namens .hgrc direkt in Eurem home-Verzeichnis an und schreibt rein:
Code: Alles auswählen
[ui]
username = Ed von Schleck <ed.von.schleck@test.com>
Jetzt tippt (wieder im asc-Quellcode-Verzeichnis) Folgendes:
Code: Alles auswählen
hg commit
Es ist üblich, eine Kurzzusammenfassung zu schreiben, die in eine Zeile passt (eine Zeile ist weniger als 80 Zeichen), und darunter einen oder mehrere Paragraphen mit einer ausführlichen Beschreibung. Dieser Patch fällt eher in die Kategorie "simpel", daher könnte man die ausführliche Beschreibung auch weglassen. Diese Commitmessages sind immer auf Englisch (wie auch Kommentare im Code). Ich schreibe also an den Anfang der Datei
Code: Alles auswählen
Changed 'player' parameter type in 'automaticTraining' to const reference
In the process I had to change the 'sigPlayerTurnEndsStatic' slot
definition as well.
Code: Alles auswählen
hg history | less
Code: Alles auswählen
Änderung: 3339:308acef4383f
Marke: tip
Vorgänger: 3336:e7a86748078d
Nutzer: Christian Schramm <christian.h.m.schramm@gmail.com>
Datum: Tue Apr 02 18:39:44 2013 +0200
Zusammenfassung: Changed 'player' parameter type in 'automaticTraining' to const reference
ein Patch-File erstellen
Dafür gibt es eine praktische Funktion von Mercurial: Ich kann ihm sagen, ein diff auszuspucken, dass die Änderungen meines letzten Commits beschreibt. Dazu schreibe ich einfach
Code: Alles auswählen
hg export tip
Code: Alles auswählen
hg export tip > patch0.diff
Ein Patch-File ist praktisch, um einen Überblick über die Änderungen zu kriegen. Es ist aber manchmal etwas frickelig, diese wieder zu integrieren, insbesondere wenn es viele und komplexe Änderungen sind. Daher gibt es eine zweite Methode.
der Pull-Request
Dazu brauchen wir ein Mirror unseres Codes. Es gibt Services im Internet, die so etwas bereitstellen. Der verbreitetste Service für Mercurial ist bitbucket.org. Da kann man sich anmelden und sein Repository spiegeln. Ich erkläre jetzt nicht, wie das geht; die Seite ist recht selbsterklärend, aber wenn Ihr Fragen habt, postet sie einfach unten.
Wenn Ihr also ein leeres Repository auf bitbucket eingerichtet habt, müsst Ihr es als remote eintragen, indem Ihr in das Unterverzeichnis .hg in die Datei hgrc in die Sektion [path] etwas Equivalentes zu
Code: Alles auswählen
bitbucket = https://edvonschleck@bitbucket.org/edvonschleck/asc
Code: Alles auswählen
hg push bitbucket default
Wenn Ihr dann TheCoder oder mir Eure Änderungen geben wollt, dann schreibt uns einfach die Adresse zu Eurem Mirror, und wir können die Änderungen selbst ziehen und in unseren Code mergen.
So! Ich hoffe, Ihr habt das alles direkt am lebenden Code nachvollzogen. Wenn nicht: Macht es! Ihr braucht die Übung, denn jetzt kommt (Trommelwirbel)
die Aufgabe
Findet selbst eine solche Stelle im Code, wo ein Parameter nicht const ist, aber sein sollte, und verbessert die Stelle so lange, bis es wieder kompiliert! Wenn Ihr auf Probleme stoßt (und das werdet Ihr wohl ), dann postet das Problem, ich werde helfen. Und wenn Ihr es geschafft habt: Postet den Patch und einen Link zu Eurem Repository. Meinen Respekt werdet Ihr Euch verdient haben, denn das ist nicht ohne.
Alles klar? Dann mal los!