[linux-l] Designfrage bzgl. Deckverwaltung

Steffen Dettmer steffen at dett.de
Mi Nov 9 22:49:27 CET 2005


* Axel Weiß wrote on Sat, Nov 05, 2005 at 12:25 +0100:
> schön dass Du diesen Thread wieder aufgreifst! Aber ich bin nicht mit 
> allem einverstanden, was Du dazu schreibst.

Prima, ich finde sowas interessant. Ich hoffe, wir nerven die Liste
nicht zu sehr, aber das Subject ist ja recht eindeutig.

> Steffen Dettmer schrieb:
> > #define ROT 0
> > 
> > namespace outerspace {
> >    int ROT; // knallt
> > };
> 
> ein gutes Beispiel!

Ja, hab ich in der Praxis leider öfter mit zu kämpfen :(

Daher in C am besten immer mit "namespace" und "modul" prefixen.

#define OUTERSPACE_COLOR_RED 0

just BTW.

> > Leider kann man enums nicht iterieren.
> 
> Das ist streng genommen richtig, es gibt aber Auswege.
> 
> enum E {Name1, Name2, Name3, count_E} e;
> for (e=Name1; e<count_E; ++e) ...

Da war ich ja erst überrascht, aber es geht gar nicht:

error: no match for `++ main()::E&' operator

Wie sollte es auch:

enum E {Name1 = 0x01, Name2 = 0x10, Name3 = 0x03, count_E = 0x00} e;

ist schliesslich gültig. Aber ++e ist ja schon falsch, weil ein
Mengenelement (wie auch States in einer Statemachine) ja keinen
(definierten, eindeutigen) Nachfolger hat :)

> > class skat_karte : public karte {
> >   friend class skat_spiel;
> >   public:
> >     typedef int farbe;
> >     static const farbe Karo  = 9;
> >     static const farbe Herz  = 10;
> >     static const farbe Pik   = 11;
> >     static const farbe Kreuz = 12;
> >
> >     farbe
> >     get_farbe(void) const;
> >
> >   protected:
> >
> >   private:
> >     // Konstruktion ist verboten. Es gibt immer genau 32 Karten.
> >     //   (aber das Deck macht das einmal)
> >     skat_karte(farbe, blatt);
> >
> >     // Kopieren ist verboten. Es gibt immer genau 32 Karten.
> >     skat_karte(const skat_karte&);
> >
> >     // Destruktion ist verboten. Es gibt 32.
> >     virtual ~skat_karte(void);
> >
> >     // Assignment ist verboten.
> >     skat_karte&
> >     operator=(const skat_karte&);
> >
> >   private:
> >     // Die farbe einer Karte ändert sich nie
> >     const farbe farbe_;
> >     const blatt blatt_; // Ass, Koenig, 7 usw.
> > };
> 
> Der Typ farbe ist immer noch int, d.h. der Compiler unterscheidet ihn 
> nicht von anderen int-Typen. Besser ist hier enum:
>     typedef enum farbe{
>         Karo  = 9,
>         Herz  = 10,
>         Pik   = 11,
>         Kreuz = 12
>     } farbe;

Dann geht aber "spielwert = farbe * spiel" nicht, weil farbe ja eben
kein int ist, und nichtmal "Karo < Pik" geht noch.

> > Verwenden dürfte man dann nur Referenzen (die viel mächtiger als
> > gleichnamige Java-Konstrukte sind, die in C++ eher Zeigern
> > nahekommen). Zeiger braucht man in C, in C++ kann man mit Referenzen
> > viel mehr reissen. Wenn man fast nur noch Referenzen benutzt, beginnt
> > man den Unterschied zu C zu verstehen ;)
> 
> Mit Deiner Definition der Klasse skat_karte kannst Du mit Referenzen aber 
> nicht mehr viel anstellen, weil alles const ist. 

Ja richtig, wie bei einer Karte! Die Farbe ändert sich nicht, der Wert
nicht, ist halt const.

> Im Gegensatz zu Zeigern, die das Objekt wechseln können, das sie
> referenzieren, ist die Zuordnung einer Referenz zum Objekt für die
> Lebenszeit der Referenz festgelegt. 

Ja, fetzt! Wenn eine Karte im Skat liegt, liegt sie halt da. Man kann
den Skat aber aufnehmen und einen neuen hinlegen. Es ist purer Zufall,
wenn der Skat die gleichen Karten enthält (geht aber natürlich).

> Sind die Objekte jetzt const (weil es const Objekte sind oder weil sie
> lauter const Member haben, ist ja egal), sind die Referenzen es auch,
> und in Deinem Spiel bewegt sich nichts.

Doch, beispielweise kann man einen Skat neu-erzeugen.

> > Dann müsste man das Deck aufbauen:
> >
> > // C++ hat keine nativen Listen. Daher hier C. Ist prakmatisch.
> > //   Die Konstruktion hier ist ne Schnellgeburt und bestimmt falsch.
> > typedef int blatt;
> > const blatt Ass = 11;
> > const blatt Zehn = 11;
> > const blatt Koenig = 4;
> > // usw.
> > const blatt blaetter[] = {
> > 	Ass,
> > 	Zehn,
> > 	Koenig,
> > 	// usw.
> > };
> 
> Das ist ja doppelt gemoppelt, Du hast jedes Blatt zweimal im Speicher...

Na ja, bei ca. 4 Byte je Blatt kann man das verschmerzen ;-)

> > Dann ein Spiel bauen:
> >
> > // "const std::vector<skat_karte> skat_spiel;" wäre schön, aber lässt
> > sich //    nicht richtig konstruieren - oder?
> >
> > class skat_spiel : private std::vector<skat_karte> {
> >   public:
> >     skat_spiel(void);
> >     ~skat_spiel(void);
> >
> >     // Karten ändern sich ja nie:
> >     const skat_karte&
> >     operator[](int karten_nummer);
> >
> >     // Zufallsziehung
> >     const skat_karte&
> >     get_irgendeine(void);
> >
> >     // und was man so braucht
> >
> >   private:
> >     skat_spiel *instance_;
> > };
> 
> Da bleiben mir aber ein paar Fragen:
> 1. wozu brauchst Du die vector-Infrastruktur? Ein simples const-Array 
> tuts hier viel billiger.

Das Deck wird ja bei der Construktion mittel "push_back" erzeugt, siehe
unten skat_spiel::skat_spiel. Ein const-Array wäre fein, wüsste aber
nicht, wie man das füllt (weil die Reihenfolge ja zufällig ist wegen
Kartenmischen). Müsste probieren, ob 

skat_spiel::skat_spiel(void)
  : array_ = {
	skat_karte(f, blaetter[0]),
	skat_karte(f, blaetter[1]),
	skat_karte(f, blaetter[2])
    }
{
}

erlaubt ist und funktioniert. Könnte bei C++ ja fast gehen :) Bei Ada
gehts jedenfalls und bei C nicht, soweit aber aus'm Bauch lol

Hab ich nicht dran gedacht, danke für den Hinweis.

> 2. Zufallsziehung: wählst Du zufällig eine Karte aus, oder merkt sich die 
> Klasse (wo?, wie?), welche Karten bereits gezogen wurden? Sonst habe ich 
> bald zwei Kreuz-Buben...

Ja, das kapselt ja die schlaue Implementierung, die ich weggelassen hab.
Ging ja um was anderes. Ich wüsste jetzt auch nicht, wie man das elegant
macht.

> 3. Was hast Du mit instance_ vor??

lol

Sorry, übrig geblieben... hab erst ein Singelton im Kopf gehabt und
gemerkt, dass das hier falsch ist. Hab das dann alles nochmal
geschrieben und den member vergessen.

> 
> > skat_spiel::skat_spiel(void)
> > {
> >    for (farbe f = Karo; farbe <= Kreuz; farbe++) {
> >       for (int blatt_nummer = 0;
> >            blatt_nummer < sizeof(blaetter)/sizeof(blaetter[0]);
> >            blatt_nummer++
> >       ) {
> >          push_back(skat_karte(f, blaetter[blatt_nummer]));
> >       }
> >    }
> >    assert(size() == 31);
> > }
> 
> Ich würde grob schätzen, dass size() hier 32 zurückgibt...

Ja, stimmt. Aber besser, als kein assert, weil ist ja schnell gefixt :-)

Solche Sachen (off-by-one) via assert zu dokumentieren und abzufangen,
find ich prima (auch wenn das Beispiel jetzt nicht so ganz gelungen ist
lol)

> Und nochmal: Du baust mühsam eine statische Struktur auf und verwendest 
> dafür ein Template, das für dynamische Strukturen erdacht wurde. 

Ach, das ist für nix und alles gedacht, daher ja der Aufriss mit
Allocator und Referencee oder wie das heisst (also, dass man technisch
zwei Templateparamter hat, der andere mit "= &T" oder so vorblegt).

Klar, const array wäre besser. Falls das geht. Dann hätte man in C++
immerhin halbe native Listen :) In Perl geht's auch, aber da gibt's kein
const. Schon komisch. 

> Wenn ich jetzt die dreißigste Karte haben will, muss sich erst ein
> Iterator 29 Mal vorwärts hangeln, bis er die Karte hat. 

Ach, machste this->[1] oder
skat_spiel deck;
karte = deck[1];
(dem Vector steht es ja frei, operator[] geschickt zu implementieren -
und das macht die STL natürlich).

> Mit Arrays geht das durch direktes Indizieren viel schneller.

Klar.

> > Den vector skat_spiel kann man jetzt nicht mischen oder sonstwie
> > kaputtkriegen (wenn ich nix vergessen hab).
> >
> > Man kann man aber sowas machen:
> >
> > // wir haben hier erstmal nur ein einziges Spiel:
> > static const skat_spiel das_eine_spiel;
> >
> > // sollen 32 const-Referenzen sein. Bin mir nicht sicher, ob die decl
> > so //   OK ist :)
> > typdef const skat_karte& karte_ref;
> > karte_ref stapel[32];
> 
> Nein, das geht nicht. Referenzen _müssen_ initialisiert werden. So wie Du 
> das machen willst, musst Du Zeiger nehmen.

ja klar. Referenzen sind richtig, for ist falsch. Damit kommt man zu
genau Deinem Vorschlag des const-Arrays.

> skat_karte *stapel[32];	// die Karten selbst sind const-Klassen

ihhh... sinnlose, uninitialisierte Zeiger ;-)

(Hat aber gegenüber meinem Bug den Vorteil, dass es kompiliert lol)

Na ja, vielleicht bin ich da besonders abgeneigt, weil ich schon
ziemlich viel code von "flexiblen" (GEHACKTEN!) Zeiger"algorithmik" auf
saubere Referenz"algorithmik" umstellen musste.

> Du hast doch schon festgestellt, dass Du nicht mischen kannst. 

Ach klar, man muss halt nur das Deck neu konstruieren. Macht ja auch
Sinn: wenn man mischt oder eine andere Teilmenge wählt, hat man ja ein
anderes Deck (oder wie der Begriff richtig heisst).

> Ehrlich gesagt, verstehe ich nicht, was Du mit der skat_karte-Klasse
> ausdrücken willst. 
> Das selbe erreicht man auch durch ein einfaches
> Array:

Moment, es war gefordert, C++ Elemente auszunutzen!

Bei einem Array ist das Operator-Overloading schwieriger, man kann
alles-oder-nichts-const-machen, nicht beerben etc.

skat_karte kann beispielsweise operator> und Freunde implementieren:

assert(HerzDame < HerzAss);
assert(HerzAss < PikSieben);

oder wie man das haben möchte, sogar wie im Skat. Unter Beachtung der
Trumpffarbe, wenn die Karte weiss, zu welchen Spiel sie gehört. Da kann
die Referenz ja const sein, solange der Rest nur gelesen wird (sprich:
farbe
getTrumpf(void) const;
deklariert ist.


(
  Natürlich ist das alles sehr konstruiert. Und wer Polymorpie brauch, um
  zwei ints zu vergleichen, macht sicherlich was falsch. Aber man
  implementiert auch keine Katzen und Hunde und trozdem sind diese
  Beispiele oft zu finden :-)
)

> static const Karte Spiel[] = {
> 	{KARO,  SIEBEN, 0},
> 	{KREUZ, AS,    11}
> };
> 
> Das ist ein starres Kartenspiel, das Deiner skat_karte-Klasse entspricht. 

Du meinst, skat_spiel entspricht? Das skat_spiel legt die Reihenfolge
zur Laufzeit fest, Spiel[] ist (hier) compilezeit statisch.

Aber klar, ein const Array wie auch immer ist besser, hatten wir ja
oben. Aber weniger C++-Features verwendet :)

> Damit spielen würde ich jetzt z.B. mit einer Index-Klasse
> 
> class Index{
> protected:
> 	size_t size, *index;

	size_t *const index;

würde ich schreiben, schliesslich ändert sich index selbst nie, dass
kann man auch versprechen ;)

> public:
> 	Index(size_t dim): size(dim), index(new size_t[dim]){
> 		for (size_t i=0; i<dim; ++i) index[i] = i;
		
warum hier kein Mix?

> 	}
> 	~Index(){delete[] index;}

warum ist das public? Darf man doch nur einmal machen?

> 	void Mix(size_t n=1000){
> 		for (size_t i=0; i<n; ++i){
> 			size_t i1 = rand() % size,
> 			       i2 = rand() % size,
> 			       tmp = index[i2];
> 			index[i2] = index[i1];
> 			index[i1] = tmp;
> 		}
> 	}
> 	size_t operator[](size_t i) const {return index[i];}

reichte nicht ein:

 	const size_t &operator[](size_t i) const {return index[i];}
?

> 	friend ostream &operator<<(ostream &o, const Index &i);
> };
> 
> Der Konstruktor stellt sicher, dass jeder Index genau einmal da ist, und 
> das Mischen erhält diese Konsistenz. 

Ja, ist aber kein "objektorientierter", sondern ein "prozeduraler"
Ansatz, finde ich. Das geht auch in C so, nur das "index" dann ein Modul
ist (jajaja, lassen wir die Details mal weg ;)).

Ich find "gleich richtig bauen und dann ist das Objekt da", ist
"objektorientierter".

> Ein Skatspiel ist dann
> 
> Index skat(32);
> 
> Auf eine Karte greift man jetzt mit
> 
> const Karte &eineKarte = Spiel[skat[x]];
> 
> zu, nachdem man das Spiel mit skat.Mix() gemischt hat.

Man mischt aber das Spiel, nicht den skat. Na gut, nicht den Index.
Also, technisch schon, aber ich würde das kapseln (erfüllt auch gleich
das Demeter-Prinzip, was man aber IMHO nicht überbewerten sollte), als
spiel.mix() aber details.

> > Das hat den Nachteil, dass hier immer noch zwei Eigentümer sein
> > könnten, die diese Karte haben.
> >
> > In der Praxis (macht man das natürlich viel einfacher, klar :) )
> > könnte man "wo_bin_ich" als Karteneingenschaft definieren, und mit
> > normalen Referenzen arbeiten, wobei es halt sowieso keine setter für
> > Farbe etc. gibt (wie ja auch im Beispiel nicht).
> 
> In der Praxis würde man Spieler haben, die Karten besitzen. Wozu sollte 
> die Karte wissen, wem sie gehört?

Wenn man das so sieht, das einer Karte ein Ort zugeordnet ist, würde ich
das so machen. Ort kann dann sein Skat, SpielerA usw.

Aber aus'm Bauch halte ich das erstmal für falsch.

> > > 3) Was anderes, was mir noch nicht eingefallen ist..
> >
> > Aus'm Bauch herraus würde ich erstmal über der Regel "Mache
> > Datenstrukturen ruhig komplex, aber den Code einfach" meditieren, oft
> > fällt einem dann was ein. Dann einen prototypen bauen. Und dann das
> > Design vereinfachen. Wenn es partou nicht mehr einfacher geht, ist es
> > fertig :)
> 
> Das sehe ich genauso! Allerdings bin ich extrem vorbelastet, was das 
> Ver(sch)wenden von Ressourcen angeht (auf Embedded Systemem hat man 
> immer zu wenig). Was hilft einem einfacher Code, der doch nicht aufs 
> Target passt...

Ja, bei embedded ist das was anderes. Da hilft auch schon viel, einfach
mal C anstatt C++ zu nehmen, läpert sich ja. Bei embedded würde ich
vielleicht aus sowas kommen:


#define SKATSPIEL_KARTE_KARO_SIEBEN = 0;
#define SKATSPIEL_KARTE_KARO_ASS    = 11;
#define SKATSPIEL_KARTE_HERZ_SIEBEN = 12; // !

Dann ist das spiel:

#pragma(pack) // platform dependent

static const unsigned char Spiel[] = {
	SKATSPIEL_KARTE_KARO_SIEBEN,
	...
};

bloss noch 32 byte gross. Und das ist noch nicht optimiert. Bleiben ja
noch einen Haufen bits übrig (32 Karten brauchen 5 bit, 3 übrig) ;)

> > Jedenfalls bekommt man es in C++ dann so hin, dass nichts geht, was
> > man nicht darf (also z.B. /kann/ man halt keine Karte kopieren. Wenn
> > sie im "skat" liegt, ist sie "const" und /kann/ sich nicht ändern
> > usw). Das ist IMHO eine grosse Stärke - das geht in Java z.B. nicht so
> > gut.
> 
> Gibt es const nicht auch in Java?

Nee, bloss "final" aber das ist was anderes. Aber ich kenn Java5 nicht.

> Wenn die Referenzen nie ungültig sein können, mag das ein Vorteil sein. 
> In der Regel möchte ich aber auch ausdrücken können, dass eine Methode 
> nichts zurückgibt, weil ein Objekt angefragt wurde, das nicht existiert 
> oder gerade nicht frei ist. Da sind NULL-Zeiger sehr praktisch.

Nein, Exceptions. Also, kommt natürlich auf den Fall an, klar. Aber wenn
ich zwei Karten aus dem Skat nehme und da ist bloss eine drin, möchte
ich nicht NULL sondern eine KarteUnterDenTischGeschummeltException :-)

War interessant! Danke für die Mail!

oki,

Steffen

-- 
Dieses Schreiben wurde maschinell erstellt,
es trägt daher weder Unterschrift noch Siegel.



Mehr Informationen über die Mailingliste linux-l