[linux-l] Designfrage bzgl. Deckverwaltung

Axel Weiß aweiss at informatik.hu-berlin.de
Sa Nov 5 12:25:58 CET 2005


Hi Steffen,

schön dass Du diesen Thread wieder aufgreifst! Aber ich bin nicht mit 
allem einverstanden, was Du dazu schreibst.

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

ein gutes Beispiel!

> 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) ...

> 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;

> 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. 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. 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.

> 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...

> 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.
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...
3. Was hast Du mit instance_ vor??

> 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...
Und nochmal: Du baust mühsam eine statische Struktur auf und verwendest 
dafür ein Template, das für dynamische Strukturen erdacht wurde. Wenn 
ich jetzt die dreißigste Karte haben will, muss sich erst ein Iterator 
29 Mal vorwärts hangeln, bis er die Karte hat. Mit Arrays geht das durch 
direktes Indizieren viel schneller.

> 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.

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

> Dann einfach die Karten der Reihe nach zu nehmen (wie in
> das_eine_spiel):
>
> for (int kartenNummer = 0; kartenNummer < 32; kartenNummer++) {
>    stapel[kartenNummer] = das_eine_spiel[kartenNummer];
> }
>
> und dann einfach per Zufall mischen.

Du hast doch schon festgestellt, dass Du nicht mischen kannst. Ehrlich 
gesagt, verstehe ich nicht, was Du mit der skat_karte-Klasse ausdrücken 
willst. Das selbe erreicht man auch durch ein einfaches Array:

typedef enum {
	KARO  = 9,
	HERZ  = 10,
	PIK   = 11,
	KREUZ = 12
} Farbe;

typedef enum {
	SIEBEN,
	ACHT,
	NEUN,
	BUBE,
	DAME,
	KOENIG,
	ZEHN,
	AS
} Wert;

struct Karte{
	Farbe  farbe;
	Wert   wert;
	size_t punkte;
};

static const Karte Spiel[] = {
	{KARO,  SIEBEN, 0},
	{KARO,  ACHT,   0},
	{KARO,  NEUN,   0},
	{KARO,  BUBE,   2},
	{KARO,  DAME,   3},
	{KARO,  KOENIG, 4},
...
	{KREUZ, KOENIG, 4},
	{KREUZ, ZEHN,  10},
	{KREUZ, AS,    11}
};

Das ist ein starres Kartenspiel, das Deiner skat_karte-Klasse entspricht. 
Damit spielen würde ich jetzt z.B. mit einer Index-Klasse

class Index{
protected:
	size_t size, *index;
public:
	Index(size_t dim): size(dim), index(new size_t[dim]){
		for (size_t i=0; i<dim; ++i) index[i] = i;
	}
	~Index(){delete[] index;}
	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];}
	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. 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.

> 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?

> > 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...

> 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?

> Das ist IMHO eine prima Übung. Ich hab ein paar Datenstrukturen, die
> änliche Eigenschaften haben (unkopierbare Objekte [unkopierbar, weil
> mit resourcen verknüpft] in Listen, alle anderen arbeiten mit Zeigern
> oder natürlich den geliebten Referenzen - letztere können ja nie NULL
> werden

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.

Gruß,
			Axel




Mehr Informationen über die Mailingliste linux-l