[C++] pointer-to-member en inheritance

Pagina: 1
Acties:

  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
Voor mijn scripting-engine (zie onder andere dit topic) heb ik een class ScriptObject gemaakt. Objecten die scriptable worden moeten deze classe implementen. Ze kunnen daarna met de functie Bind(wstring, Member) een member functie binden aan een string via de map. Via Execute, die door de scripting engine wordt aangeroepen (als inderdeel van de class Scriptable overigens) wordt dan de juiste member-functiepointer opgezocht en uitgevoerd:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
template<typename T> class ScriptObject: public virtual Scriptable {
            protected:
                ScriptObject() {
                }

                virtual ~ScriptObject() {
                }
                
                typedef ref<Scriptable> (T::*Member)(ref<ParameterList>);
                typedef std::map<CommandType, Member> MemberMap;

                void Bind(Command c, Member p) {
                    _members[c] = p;
                }

            public:
                virtual ref<Scriptable> Execute(Command c, ref<ParameterList> p) {
                    MemberMap::const_iterator it = _members.find(c);
                    if(it!=_members.end()) {
                        Member m = it->second;
                        return (((T*)this)->*m)(p);
                    }
                    return 0;
                }

            protected:
                typename MemberMap _members;
        };

// Voorbeeld implementatie:

class MyScriptableObject: public ScriptObject<MyScriptableObjet> {
....
MyScriptableObject() {
   Bind(L"toString", &MyScriptableObject::ToString);
}

ref<Scriptable> ToString(ref<ParameterList> p) {
   return new ScriptString(L"Hello world");
}
}


Waarin CommandType=std::wstring en de ref<> dingen gewoon refcounted pointers zijn, maar dat doet er hier niet toe :)

Wat het probleem nu is, is dat het niet werkt! Ik krijg een dikke nullpointer (0xC0000005) bij het aanroepen van (in dit geval) toString via het script. Aan de scripting engine ligt het niet, want Execute wordt netjes aangeroepen. De fout lijkt te zitten in het stukje daarna. Ik wil vanuit de base class (die getemplate is met T=naam van child class, ben de naam van dat pattern even kwijt...) dus een member function pointer gebruiken naar een member function van de child class! Wat doe ik fout?

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

H!GHGuY

Try and take over the world...

wil je niet iets wat meer lijkt op dit:

C++:
1
T::*m(p)

ASSUME makes an ASS out of U and ME


  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
H!GHGuY schreef op zaterdag 14 april 2007 @ 12:42:
wil je niet iets wat meer lijkt op dit:

C++:
1
T::*m(p)
Dat geeft een hele mooie:
error C2059: syntax error : '<tag>::*'
Dus dat zal wel niet mogen :) Wat ik trouwens nog bij mijn code hierboven kan toevoegen is dat het met dynamic_cast<T*> ook niet werkt (er staat nu (T*) om te casten)...

  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
Okee, het lijkt nu te werken. De truc was om de pointer Derived::functie te casten naar Base::functie. Kennelijk zijn die twee pointers niet hetzelfde (andere v-table ofzo?) en kunnen ze zelfs in grootte verschillen volgens wat websites. Ik had niet door dat die dingen gewoon gecast kunnen worden, maar na dat gedaan te hebben werkt het vlekkeloos :)

  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
Nog iets anders: ik wil de map graag static maken, zodat ik niet voor iedere instantie van een class een aparte map heb met bindingen. Aangezien de member pointers hetzelfde zijn, mag dat volgens mij gewoon:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template<typename T> class ScriptObject: public Scriptable {
            protected:
                ScriptObject() {
                }

                virtual ~ScriptObject() {
                }
                
                typedef tj::shared::ref<Scriptable> (T::*Member)(tj::shared::ref<ParameterList>);
                typedef tj::shared::ref<Scriptable> (ScriptObject::*BaseMember)(tj::shared::ref<ParameterList>);
                typedef std::map<CommandType, BaseMember> MemberMap;

                static void Bind(Command c, Member p) {
                    ScriptObject<T>::_members[c] = (BaseMember)p;
                }

                /** This calls the static method ::Initialize on T. **/
                struct ScriptObjectInitializer {
                    ScriptObjectInitializer() {
                        T::Initialize();
                    }
                };

            public:
                virtual tj::shared::ref<Scriptable> Execute(Command c, tj::shared::ref<ParameterList> p) {
                    MemberMap::const_iterator it = _members.find(c);
                    if(it!=_members.end()) {
                        BaseMember m = it->second;
                        return (this->*m)(p);
                    }
                    return 0;
                }

            protected:
                static typename ScriptObjectInitializer _initializer;
                static typename MemberMap _members;
        };

        template<typename T> typename ScriptObject<T>::ScriptObjectInitializer ScriptObject<T>::_initializer = typename ScriptObject<T>::ScriptObjectInitializer();
        template<typename T> typename ScriptObject<T>::MemberMap ScriptObject<T>::_members = typename ScriptObject<T>::MemberMap();


De ScriptObjectInitializer is static en wordt dus aangeroepen als de class bekend wordt. Dan zou de map moeten worden gevuld. Maar bij het opstarten van de applicatie krijg ik 'Application failed to initialize correctly (0xC0000005)'.... iemand enig idee?

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 07:20

.oisyn

Moderator Devschuur®

Demotivational Speaker

MisterData schreef op zaterdag 14 april 2007 @ 19:14:
Okee, het lijkt nu te werken. De truc was om de pointer Derived::functie te casten naar Base::functie
Je kan een Derived::* helemaal niet casten naar een Base::*. Dat kan alleen de andere kant op. Think about it: je maakt een Derived::* die wijst naar Derived::func. Als je die method aanroept dan verwacht die minstens een Derived this pointer. Als je 'm zou kunnen casten naar Base::*, dan betekent dat je die method ook aan zou kunnen roepen op een Base pointer. En dus roep je Derived::func aan met een Base* als this, maar die Base* is helemaal niet gegarandeerd een Derived*. Je mag dus geen Derived::* op een Base* aanroepen, en derhalve kan die cast ook niet.
MisterData schreef op zaterdag 14 april 2007 @ 19:14:
Okee, het lijkt nu te werken. De truc was om de pointer Derived::functie te casten naar Base::functie. Kennelijk zijn die twee pointers niet hetzelfde (andere v-table ofzo?) en kunnen ze zelfs in grootte verschillen volgens wat websites. Ik had niet door dat die dingen gewoon gecast kunnen worden, maar na dat gedaan te hebben werkt het vlekkeloos :)
Rare argumentatie, de pointer waar je de method op aanroept is al een Derived* (want je cast de ScriptObject* immers naar een T*). Je code zou gewoon moeten werken. Welke compiler gebruik je?

Ik gok dat je huidige code (dus met cast van de memberfunctionpointer) keihard op z'n bek gaat als je ervoor zorgt dat de pointers tussen een MyScriptableObject en een ScriptObject<T> niet meer identiek zijn (door bijvoorbeeld een andere base class (met members) van MyScriptableObject voor ScriptObject<T> te definieren)

[ Voor 39% gewijzigd door .oisyn op 15-04-2007 04:49 ]

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
.oisyn schreef op zondag 15 april 2007 @ 04:39:
[...]
Je kan een Derived::* helemaal niet casten naar een Base::*. Dat kan alleen de andere kant op.
Gek genoeg laat de compiler het me wel zonder meer doen... MSVC 8 trouwens :)
Think about it: je maakt een Derived::* die wijst naar Derived::func. Als je die method aanroept dan verwacht die minstens een Derived this pointer. Als je 'm zou kunnen casten naar Base::*, dan betekent dat je die method ook aan zou kunnen roepen op een Base pointer. En dus roep je Derived::func aan met een Base* als this, maar die Base* is helemaal niet gegarandeerd een Derived*. Je mag dus geen Derived::* op een Base* aanroepen, en derhalve kan die cast ook niet.
Zo uitgelegd lijkt dat inderdaad te kloppen. Het gekke is dat het nu wel goed werkt, ook als MyScriptableObject allerlei velden en nog andere rommel bevat (geen andere base class overigens, maar dat is hier ook niet nodig).

Het liefst wil ik het ntauurlijk wel 'netjes' werkend hebben, maar de vraag is dan: hoe roep ik vanuit de getemplate base-class een member-functie van de derived class aan via een pointer? (this->*p) syntax werkt alleen in de Derived class zelf en geeft een foutmelding als ik deze in ScriptObject gebruik (immers p is een Derived::functiepointer en geen Base::functiepointer, om het zo maar even te noemen).

De this-pointer casten naar een Derived* (hoe lelijk dat ook is als dat in de Base class gebeurt) werkt ook niet (ik weet niet meer precies wat daar de runtime-fout was...). Ik zou natuurlijk in de constructor van ScriptObject een argument MyScriptableObject* kunnen toevoegen, zodat de this-pointer van MyScriptableObject bekend is bij ScriptObject.

Overigens is het probleem met de statische map opgelost; ik realiseerde me dat de compiler niet kan garanderen in welke volgorde de statische objecten worden geinitialiseerd. Het zou dus sowieso link zijn geweest om het zo op te lossen. Ik heb het opgelost door bij het instantiëren van MyScriptableObject te checken of de statische methode Initialize al is aangeroepen (via een of ander static bool vlaggetje) :)

[ Voor 7% gewijzigd door MisterData op 15-04-2007 23:17 ]


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 07:20

.oisyn

Moderator Devschuur®

Demotivational Speaker

MisterData schreef op zondag 15 april 2007 @ 23:13:

Gek genoeg laat de compiler het me wel zonder meer doen... MSVC 8 trouwens :)
Om dezelfde reden dat je een downcast ook zonder meer kunt doen. Het betekent echter nog niet dat het goed gaat als je het vervolgens gebruikt :)
Zo uitgelegd lijkt dat inderdaad te kloppen. Het gekke is dat het nu wel goed werkt, ook als MyScriptableObject allerlei velden en nog andere rommel bevat (geen andere base class overigens, maar dat is hier ook niet nodig).
Als MyScriptableObject nog andere rommel bevat dan zijn de rauwe pointers naar base en derived nog steeds gelijk (omdat de base (in MSVC) qua memory layout in het begin van de derived class staat - door een andere base als eerst te declareren staat die base aan het begin, en MyScriptableObject niet meer, resulterende in verschillende pointers voor base en derived)
Het liefst wil ik het ntauurlijk wel 'netjes' werkend hebben, maar de vraag is dan: hoe roep ik vanuit de getemplate base-class een member-functie van de derived class aan via een pointer? (this->*p) syntax werkt alleen in de Derived class zelf en geeft een foutmelding als ik deze in ScriptObject gebruik (immers p is een Derived::functiepointer en geen Base::functiepointer, om het zo maar even te noemen).
Logisch, je roept 'm aan op een Base, dus je zal die this-pointer eerst moeten casten. Zoals je al deed in je originele code. Nogmaals, met je code is niets verkeerd, je probleem ligt ergens anders. Dit werkt prima bij mij (MSVC++ 8 )
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>

template<class T>
struct Base
{
    int i;

    typedef void (T::*memfunc_t)();

    Base() : i(1)
    {
    }

    void foo()
    {
        std::cout << __FUNCSIG__ << ": i=" << i << std::endl;
    }

    void call(memfunc_t pFunc)
    {
        (static_cast<T*>(this)->*pFunc)();
    }
};

struct Base2
{
    int a;
};

struct Derived : public Base2, public Base<Derived>
{
    int j;
    Derived() : j(2)
    {

    }

    void bar()
    {
        std::cout << __FUNCSIG__ << ": j=" << j << std::endl;
    }

    void work()
    {
        call(&Derived::bar);
        call(&Base::foo);
    }
};

int main()
{
    Derived d;
    d.work();
}
De this-pointer casten naar een Derived* (hoe lelijk dat ook is als dat in de Base class gebeurt)
Mwoa, lelijk... Het is by design. Je vereist dat je template parameter de type is van de derived, dus als je code correct gebruikt wordt weet je zeker dat de this ook daadwerkelijk een derived is.
Overigens is het probleem met de statische map opgelost; ik realiseerde me dat de compiler niet kan garanderen in welke volgorde de statische objecten worden geinitialiseerd. Het zou dus sowieso link zijn geweest om het zo op te lossen. Ik heb het opgelost door bij het instantiëren van MyScriptableObject te checken of de statische methode Initialize al is aangeroepen (via een of ander static bool vlaggetje) :)
C++:
1
2
3
4
5
MyMap & GetMap()
{
    static MyMap map;
    return map;
}

;)
(niet thread-safe overigens, als twee threads GetMap() tegelijkertijd aanroepen terwijl hij nog niet is gecreëerd dan zijn de rapen gaar)

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Verwijderd

MisterData schreef op zondag 15 april 2007 @ 23:13:
[...]


Gek genoeg laat de compiler het me wel zonder meer doen... MSVC 8 trouwens :)
compilers zijn dan ook erg leuke dingen... zeker als je gaat casten kan je een hoop naredingen tegen de compiler vertellen die dat zonder problemen slikt ;)

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 07:20

.oisyn

Moderator Devschuur®

Demotivational Speaker

Probeer je originele code trouwens eens met de /vmg compiler optie. Ddit zorgt ervoor dat alle pointer-to-member-functions dezelfde grootte hebben (16 bytes), ipv dat de grootte afhangt van de klassedefinitie: single inheritance = 4 bytes, multi inheritance = 8 bytes, virtual inheritance = 12 bytes, een incompleet type (dus alleen gedeclareerd, niet gedefinieerd) = 16 bytes.

[ Voor 70% gewijzigd door .oisyn op 16-04-2007 13:18 ]

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
Vreemd, je gebruikt hier een static_cast<T*>(this) en dat werkt inderdaad prima (bedankt!). Ik had het eerst met een dynamic_cast<T*>(this) gedaan, maar dat gaf problemen... enig idee waarom? :)

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 07:20

.oisyn

Moderator Devschuur®

Demotivational Speaker

Wat voor problemen? De dynamic_cast kan mogelijk een 0 retourneren, maar dat lijkt me hier sowieso niet het geval aangezien je weet dat je this minstens een T is (of je gebruikt je eigen klasse fout ;)). Wellicht dat je rare resultaten krijgt als je het gebruikt met RTTI disabled, maar volgens mij krijg je dan gewoon een compiler warning/error.

.edit: oh, dynamic_cast kan ook fout gaan als de base class in kwestie (ScriptObject<MyScriptableObject> dus) private is :). De static_cast zou in dat geval ook niet mogen, maar ik zie dat MSVC++ 8 het toch toelaat.

[ Voor 25% gewijzigd door .oisyn op 16-04-2007 14:23 ]

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


  • MisterData
  • Registratie: September 2001
  • Laatst online: 27-11 20:42
.oisyn schreef op maandag 16 april 2007 @ 14:19:
Wat voor problemen? De dynamic_cast kan mogelijk een 0 retourneren, maar dat lijkt me hier sowieso niet het geval aangezien je weet dat je this minstens een T is (of je gebruikt je eigen klasse fout ;)). Wellicht dat je rare resultaten krijgt als je het gebruikt met RTTI disabled, maar volgens mij krijg je dan gewoon een compiler warning/error.

.edit: oh, dynamic_cast kan ook fout gaan als de base class in kwestie (ScriptObject<MyScriptableObject> dus) private is :). De static_cast zou in dat geval ook niet mogen, maar ik zie dat MSVC++ 8 het toch toelaat.
De base class was niet private, RTTI stond aan en toch kreeg ik een 0xC0000005... waarschijnlijk een rare buildfout dan :) Het werkt nu overigens prima allemaal!
Pagina: 1