[C++] Child access tot parent class?

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • maleadt
  • Registratie: Januari 2006
  • Laatst online: 26-09 11:56
Dag tweakers! Klaar voor nog een beginnersvraag over C++? :+

Ik heb momenteel een programma hiërarchisc met objecten opgebouwd (World -> Speler -> Skills -> ...) waarbij ik bij elk paar objecten op een design-probleem stuit. Om het probleem te vereenvoudigen behandel ik enkel de 2 top klassen: World en Speler. Het World object heeft tal van data en routines, en ook een lijst met spelers (pointers naar objecten van het type Speler). Het object Speler staat in voor alles wat de speler doet, en heeft dus ook functies zoals move(), ...
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class World
{
    public:
        World();
        ~World();
        
    private:
        std::list<Speler*> dataSpelers;
};

class Speler
{
    public:
        Speler();
        ~Speler();
        World* parent;
        
        move();
    
    private:
        std::list<Skills*> dataSkills;


Maar om een Speler te kunnen doen bewegen (move()), moet ik toegang hebben tot zijn parent class, de World dus. En dat laatste lukt me dus maar niet :) Ik had er aan gedacht om de klasse Speler een pointer naar zijn parent te geven (World* parent), maar dat zorgt voor een kruisverwijzing: om een pointer van het type World te maken moet ik "world.h" includen, wat niet wil lukken aangezien ik in het object World een lijst std::list<Speler*> gedefiniëerd heb.

Is dit een design fout, of kan dit syntaxisch opgelost worden? de move() call verhuizen naar het World object zou alles oplossen, maar dat kan ik niet doen omdat hetzelfde probleem zich op lagere lagen nog voordoet, en dan kan ik niet zo even de routine verhuizen.

Ik heb ook nog voorbeelden tegengekomen die gebruik maken van de "class A:public B" syntax, maar dat werkte ook niet direct, AFAIK ook door deze kringverwijzing.

Alvast bedankt,
maleadt

Acties:
  • 0 Henk 'm!

  • Arjan
  • Registratie: Juni 2001
  • Niet online

Arjan

copyright is wrong

Pointers kun je altijd forward declaraten, aangezien de compiler alleen maar ruimte voor een pointer hoeft te reserveren. De volledige implementatie hoeft in de header dus nog niet bekend te zijn.

Zo min mogelijk headers includen in je headers, dit versnelt ook je compile proces omdat je preprocessor het wat makkelijker krijgt.

voor de duidelijkheid, dit in reactie op
MALEADt schreef op zaterdag 08 november 2008 @ 19:43:

[...]

Ik had er aan gedacht om de klasse Speler een pointer naar zijn parent te geven (World* parent), maar dat zorgt voor een kruisverwijzing: om een pointer van het type World te maken moet ik "world.h" includen, wat niet wil lukken aangezien ik in het object World een lijst std::list<Speler*> gedefiniëerd heb.

[...]

[ Voor 43% gewijzigd door Arjan op 08-11-2008 19:48 ]

oprecht vertrouwen wordt nooit geschaad


Acties:
  • 0 Henk 'm!

  • maleadt
  • Registratie: Januari 2006
  • Laatst online: 26-09 11:56
Problem fixed, thanks :)

Werkende code was:

world.h
C++:
1
2
3
4
5
6
7
8
9
class World
{
    public:
        World();
        ~World();
        
    private:
        std::list<Speler*> dataSpelers;
};


speler.h
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
class World;

class Speler
{
    public:
        Speler();
        ~Speler();
        World* parent;
        
        move();
    
    private:
        std::list<Skills*> dataSkills;


waarbij forward declaration via class World ; perfect werkte :) thx

Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Waarom moet Speler de World aanpassen bij het verplaatsen?

C++:
1
2
3
4
5
void Speler::move(Vector2D v)
{
  x += v.x;
  y += v.y;
}


Wil je collision detection, dan doe je bvb:
C++:
1
2
3
4
5
6
World::MoveObject(ObjectRef ref, Vector2D )
{
  WorldObject* obj = GetObject(ref);
  v = CalculateMove(obj, v)); // hier collision detection doen
  obj->move(v);
}


Ik gebruik nog wat extra abstracties die je al dan niet kan gebruiken. Je moet je bij elke pointer (zeker als het een member is van een class) afvragen of beide objecten elkaar echt wel zo innig moeten kennen.

Speler mag dan al een plaats hebben in de World, moet hij ook echt wel de World kennen? Wat speler betreft is zijn plaats niet meer dan een x,y(,z) combinatie.

Vermijd dus cross-references waar nodig. Desnoods maak je een derde class GameController die een World en Spelers als members heeft en die de boel aanstuurt. Bekijk misschien ook even smart pointers (in boost bvb), die kunnen soms pointer-geklungel verzachten. Maar hoe je ook met pointers omgaat, niets gaat boven een goed design.

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

  • maleadt
  • Registratie: Januari 2006
  • Laatst online: 26-09 11:56
Om een speler te verplaatsen is het idd een betere oplossing om een "move vector" van elke child aan te vragen, en de world zelf dan collision detection de laten uitvoeren. Maar bij andere situaties, waar min of meer hetzelfde probleem voorkomt, lijkt me de pointer naar de parent beter? Bij items bijvoorbeeld, ik vind het makkelijker om een Item te Executen, en die dan de Player correct aan te passen (HP+=10; AP-=5, ammo+=3%, ...), dan bijvoorbeeld voor elke mogelijke eigenschap een waarde aan het Item te requesten.

Los daarvan heb ik nog altijd een probleem met de forward declaration. Ik kan nu wel in de Class defenitie een pointer naar World definiëren, maar wanneer ik in een class function een waarde van die pointer opvraag, krijg ik errors. Dus:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class World;

class Creature
{
    public:
        Creature();
        ~Creature();
        World* pointerWorld;
        void DoSomething();
};

void Creature::DoSomething()
{
    int x = pointerWorld->sizeX;
}


Dit wil niet compilen, G++ klaagt dat ik gebruik maak van een onvolledige class World. Maar als ik de routines die ik nodig heb toevoeg aan de forward declaration van World, klaagt G++ dat hij een dubbele defenitie van de class World heeft. (vergelijkbaar probleem)

Acties:
  • 0 Henk 'm!

  • ATS
  • Registratie: September 2001
  • Laatst online: 26-09 11:54

ATS

Je moet in je C++ natuurlijk wel even world.h includen. Vergeet ook niet je class definities te voorzien van de standaard constructie om dubbele definities te voorkomen:
C++:
1
2
3
4
5
6
7
#ifndef MYCLASS
#define MYCLASS
class MyClass{
 <bla blah blah>
};

#endif

Zo kan je je definitie net zo vaak includen als je wil, maar hij komt maar één keer in je code terecht.

Ook moet je je pointer naar World natuurlijk wel initialiseren. Dat zou ik doen in de constructor van Creature, waarin je dan een pointer naar het World object meegeeft.

My opinions may have changed, but not the fact that I am right. -- Ashleigh Brilliant


Acties:
  • 0 Henk 'm!

  • writser
  • Registratie: Mei 2000
  • Laatst online: 21:00
Dat klopt: je kan wel aangeven dat je een pointer naar World* gebruikt: de compiler hoeft dan alleen ruimte te reserveren voor een pointer. Maar je kan 'm niet daadwerkelijk gebruiken - dan zit je weer in het recursieve probleem. De oplossing is om de declaratie te doen in een header-file en de definitie in een c++ file. Dus zoiets:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
// player.h
#ifndef PLAYER_H
#define PLAYER_H

// forward declaration: je geeft alleen aan dat deze klasse bestaat.
// Je kan 'm dus nog niet gebruiken in dit header-bestand.
class World;

class Player {
  World * w;
  void doMove();
}
#endif


C++:
1
2
3
4
5
6
7
8
// player.cpp
#include "player.h"
#include "world.h"
Player::doMove() {
  // Hier heb je de world.h header wel geinclude 
  // en kun je daadwerkelijk met de pointer werken.
  world->foobar();
}

[ Voor 4% gewijzigd door writser op 10-11-2008 12:51 ]

Onvoorstelbaar!


Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

MALEADt schreef op maandag 10 november 2008 @ 12:09:
Om een speler te verplaatsen is het idd een betere oplossing om een "move vector" van elke child aan te vragen, en de world zelf dan collision detection de laten uitvoeren. Maar bij andere situaties, waar min of meer hetzelfde probleem voorkomt, lijkt me de pointer naar de parent beter? Bij items bijvoorbeeld, ik vind het makkelijker om een Item te Executen, en die dan de Player correct aan te passen (HP+=10; AP-=5, ammo+=3%, ...), dan bijvoorbeeld voor elke mogelijke eigenschap een waarde aan het Item te requesten.
Kun je de parent pointer dan niet aan de functie meegeven:

C++:
1
2
3
4
void Item::Execute(Speler* speler)
{
  speler->AddHPBonus(100, 50); // AddHPBonus(uint hp, uint timeout)
}

Dit is bijna double dispatch, een techniek die je misschien ook wel eens kan onderzoeken. Op wikipedia staat zelfs een mooi game-gerelateerd voorbeeldje.

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

  • maleadt
  • Registratie: Januari 2006
  • Laatst online: 26-09 11:56
Ah inderdaad, includen van de parent header (hier: world.h) lostte dit probleem op :)

En de pointer passen aan de Execute functie kan ik inderdaad doen, maar dat verschilt toch dan niet zo erg van eerst een "addParent(Speler* speler)" te doen, en dan de Execute functie zonder argumenten te callen? Dat doe ik nu alleszinds. Maar het artikel over double dispatch ziet er wel interessant uit, bedankt.

Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Bekijk gelijk ook eens de Law of Demeter (Principle of least knowledge).

Het onderhouden van dergelijke backpointers kost veel moeite en is veelal een bron van problemen wanneer de structuur complexer wordt. De hoofdvraag is altijd: welk van de 2 objecten is verantwoordelijk voor het opzetten (new/add), in stand houden (gebruiken) en verbreken (delete/remove) van de relatie? Als de antwoorden op alle 3 de vragen dezelfde is dan heb je gewoon een vorm van containment en kan je eventueel een (weak) pointer bijhouden; Is er minstens 1 afwijkende dan heb je een recept voor miserie ;)

Het punt, zeker bij complexere software, is om elke class zo weinig mogelijk kennis te geven van andere classes. Hiermee creëer je een zogenaamd "loosely coupled" structuur. Voordelen hiervan zijn het eenvoudiger hergebruik en het eenvoudiger aanpassen aan veranderende requirements.
Zeker wanneer je in een groep werkt heeft dit nog voordelen. Als ik verkies om de Speler enkel mee te geven in functie X, Y en Z dan is mijn mede programmeur niet snel gezind om mijn code aan te passen en een backpointer toe te voegen. Dit houdt de code op zijn beurt weer losser gekoppeld met de voordelen vandien.

Je moet in deze zeker letten op navigeerbaarheid van de pointer. Moet een Item per se aan de Speler kunnen? Is er een code pad waar een functie van Item aan Speler moet kunnen zonder dat de callstack aan Speler passeert?
C++:
1
2
3
4
5
6
7
8
9
void World::ExecuteItem(Item* item)
{
  Item->Execute();
}

void Item::Execute()
{
  parent->AddHPBonus(100, 50);
}

of beter:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void World::ExecuteItem(SpelerID spID, ItemID itID)
{
  Speler* speler = GetSpeler(spID);
  speler->ExecuteItem(itID);
}

void Speler::ExecuteItem(ItemID ID)
{
  Item* item = GetItem(ID);
  item->Execute(this);
}

void Item::Execute(Speler* speler)
{
  speler->AddHPBonus(100,50);
}


Pas wanneer je vanaf beide classes over de relatie moet kunnen navigeren is een backpointer mogelijk een geldige oplossing. Gebruik dan liefst ook een manier om deze relatie te vereenvoudigen (bvb boost::shared_ptr voor de forward pointer en boost::weak_ptr voor de backpointer). Dit drukt meteen uit wie verantwoordelijk is voor de relatie. Maar zelfs met deze hulpmiddelen blijven backpointers het laatste redmiddel. Gebruik desnoods een 3de class die een bepaalde verantwoordelijkheid op zich neemt of gebruik references (zie de SpelerID en ItemID die ik hierboven gebruik).

ASSUME makes an ASS out of U and ME

Pagina: 1