Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien

[C++] Inheritance en templates

Pagina: 1
Acties:

  • DRAFTER86
  • Registratie: April 2002
  • Laatst online: 19:31
De afgelopen dagen ben ik bezig met het ontwikkelen van een module die de resultaten van een simulatie code die we hier gebruiken moet wegschrijven. De code is in C++.
Mijn idee was om een base class 'OutputWriter' te maken, waarvan verschillende implementaties (ASCIIWriter, BinaryWriter, MysqlWriter etc, etc) overerven.
Vervolgens moet elke implementatie methoden implementeren die verschillende (data)typen kunnen wegschrijven (double, int, verschillende objecten).
Nu was mijn eerste idee iets als (note: ik heb alle code zoveel mogelijk uitgekleed...):
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Writer{
public:
    template <class T>
    virtual void write(string name, T value)=0;
};

class AsciiWriter{
public:
    template <class T>
    void write(string name, T value){
        cout<<value<<endl;
    }
}

Maar dat werkt niet, virtual methods kunnen niet ge-template worden, wat natuurlijk (enigszins) logisch is.
Nu kwam ik na wat omzwervingen (note: mijn C++ kennis, en vooral mijn design-pattern kennis is nou niet bepaald geweldig) op het Curiously recurring template pattern.
Ofwel, een vorm van static polymorphisme, die mij het volgende laat doen:
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
#include <iostream>
using namespace std;

template <class Derived>
class Writer{
public:
    template <class T>
    void write(T value)
    {
        static_cast<Derived*>(this)->write(value);
    }
};

class SomeWriter : public Writer<SomeWriter > {
public:
    template <class T>
    void write(T value){
        cout<<"SomeWriter: "<<value<<endl;
    }
};
class AnotherWriter : public Writer<AnotherWriter > {
public:
    template <class T>
    void write(T value){
        cout<<"AnotherWriter: "<<value<<endl;
    }
};

int main(){
    SomeWriter s;
    s.write("tekst");
    AnotherWriter a;
    a.write(1.3);
}

Dus de implementaties SomeWriter en AnotherWriter geven zichzelf als template parameter mee aan de base class, waardoor deze via een static_cast de methode 'write()' van de Derived class kan aanroepen...
Uiteraard zijn in mijn uiteindelijke code de write() methoden van de verschillende implementaties compleet anders ;)
Nu, dit komt erg dicht in de buurt van wat ik wil, maar....
Hiermee kan ik nog niet at runtime kiezen welke implementatie ik gebruik zoals ik met dynamic polymorphisme zou kunnen doen. Uiteindelijk wil ik natuurlijk iets doen als:
C++:
1
2
3
4
5
6
OutputWriter* w;
if(...){
    w = SomeWriter();
}else{
    w = AnotherWriter();
}

Wat uiteraard niet compiled omdat de declaratie van een 'OutputWriter* w' pointer een template argument mist...
Nu, ik heb vaag het idee dat wat ik wil gewoon tegen het strong-type principe van C++ indruist...
Maar met het plaatsen van dit topic hoop ik enerzijds duidelijk te krijgen of wat ik wil inderdaad gewoon niet kan, of anderzijds een oplossing of alternatief te vinden...

  • EddoH
  • Registratie: Maart 2009
  • Niet online

EddoH

Backpfeifengesicht

In dit geval zou ik het KISS principe aanhouden. Gewoon een interface definiëren met (pure)virtual write methodes voor elk datatype, die interface implementeer je dan in je verschillende writers. Tenzij je echt een goede reden hebt om het anders te doen, ruikt dit een beetje naar over-engineering.

[ Voor 24% gewijzigd door EddoH op 13-06-2013 09:27 ]


  • farlane
  • Registratie: Maart 2000
  • Laatst online: 13:12
Even voor mijn beeld, waarom wil je hier perse een template gebruiken? Waarom niet de voor de hand liggende methode waarbij je wat virtual methods override in je afgeleide classes?

[edit]
Met EddoH dus

[ Voor 6% gewijzigd door farlane op 13-06-2013 09:26 ]

Somniferous whisperings of scarlet fields. Sleep calling me and in my dreams i wander. My reality is abandoned (I traverse afar). Not a care if I never everwake.


  • DRAFTER86
  • Registratie: April 2002
  • Laatst online: 19:31
Nou, er zijn een heleboel datatypen (classes), en er komen ook regelmatig datatypen bij.
In HDF5 (het gebruikte fileformat) heeft elk object dat je wegschrijft een datatype nodig (waarin de vertaling van de C++ memory-layout naar een HDF5 datatype word beschreven).
Dus stel dat ik een class Particle heb, dan moet ik een HDF5 datatype definiëren waarin ik specificeer welke members van mijn Particle object overeenkomen met welke HDF5-object members.
Ik wil dus niet dat elke keer als er een nieuw datatype bij komt men ook een virtual method aan alle outputwriters moeten toevoegen. Het zou genoeg moeten zijn om het benodigde datatype te defineren...
Hopelijk verklaard dit een beetje de omslachtige aanpak, zo niet, dan hoor ik het wel :)

  • EddoH
  • Registratie: Maart 2009
  • Niet online

EddoH

Backpfeifengesicht

Je moet toch sowieso weten hoe je het betreffende datatypen moet gaan 'writen' in je Writer classes?
Je kunt ook nog alle datatypen laten overerven van een generieke Writer interface. Dan heeft elk object dus een 'write(..)' methode die je kan aanroepen vanuit je Writer classes, en hoef je tevens maar 1 write() methode in je Writer interface te definieren.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WriteableObject
{
public:
  virtual ~WriteableObject() {}

  virtual void write(char * destBuf) = 0;

};

class OutputWriterIntf
{
public:
  virtual ~OutputWriterIntf() {}

  virtual void writeObject(WriteableObject *aObject) = 0;
};

  • Jewest
  • Registratie: Juni 2007
  • Laatst online: 19-11 14:32
Wat je zou kunnen doen is in the basis klasse de verschillende types data afhandelen.
Deze worden standaard geconverteerd naar strings en in je overerfde klassen klassen herdefineer je de string write functie. Op deze manier is het vrij makkelijk om data types to te voegen in alle overerfde klassen. ook met de Read functie kun je dit doen, maar dan andersom.

Flickr
Canon 7D + Glas + Licht
Komt het rot over dan bedoel ik het anders en taalfouten zijn inbegrepen.


  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
EddoH schreef op donderdag 13 juni 2013 @ 09:44:
Je moet toch sowieso weten hoe je het betreffende datatypen moet gaan 'writen' in je Writer classes?
Je kunt ook nog alle datatypen laten overerven van een generieke Writer interface. Dan heeft elk object dus een 'write(..)' methode die je kan aanroepen vanuit je Writer classes, en hoef je tevens maar 1 write() methode in je Writer interface te definieren.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WriteableObject
{
public:
  virtual ~WriteableObject() {}

  virtual void write(char * destBuf) = 0;

};

class OutputWriterIntf
{
public:
  virtual ~OutputWriterIntf() {}

  virtual void writeObject(WriteableObject *aObject) = 0;
};
Het nadeel daarvan is wel dat je de logica over het wegschrijven in het object zelf zet. Als je straks meerdere manieren wil hebben om objecten weg te schrijven is dat weer lastiger ( Bijvoorbeeld JSON, XML, Binair ). Maar dat is een afweging die je moet maken, als je niet verwacht dat dat snel gaat gebeuren is het natuurlijk een prima oplossing, en je kunt het altijd nog refactoren.

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


  • EddoH
  • Registratie: Maart 2009
  • Niet online

EddoH

Backpfeifengesicht

Heb je gelijk in. Het is 1 van de vele oplossingen mogelijk. De TS moet zelf afweging maken wat in zijn systeem het beste werkt en het minste onderhoud vergt. Het is maar een suggestie.

  • DRAFTER86
  • Registratie: April 2002
  • Laatst online: 19:31
EddoH schreef op donderdag 13 juni 2013 @ 09:44:
Je moet toch sowieso weten hoe je het betreffende datatypen moet gaan 'writen' in je Writer classes?
Je kunt ook nog alle datatypen laten overerven van een generieke Writer interface. Dan heeft elk object dus een 'write(..)' methode die je kan aanroepen vanuit je Writer classes, en hoef je tevens maar 1 write() methode in je Writer interface te definieren.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WriteableObject
{
public:
  virtual ~WriteableObject() {}

  virtual void write(char * destBuf) = 0;

};

class OutputWriterIntf
{
public:
  virtual ~OutputWriterIntf() {}

  virtual void writeObject(WriteableObject *aObject) = 0;
};
Klopt, de link tussen een HDF5-datatype en de bijbehorende C++ heb je sowieso nodig.
Misschien had ik het meteen moeten doen, maar hier een wat completer voorbeeld:
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include <iostream>
#include <vector>
using namespace std;

//De class BaseType dient enkel als base.
template <class Child>
class BaseType {
};

//Een string-implementatie van BaseType volgens het CRT pattern, 
class StringType : public BaseType<StringType>{
public:
    StringType(string _s){
        set(_s);
    }
    string get(){
        return s;
    }
    void set(string _s){
        s=_s;
    }
    static void getTypes(){
        /*
        In een echte HDF5-implementatie word hier het HDF5-datatype geconstrueerd.
        Dit houdt onder andere het berekenen van de memory offsets van 
        verschillende object-members etc
        */
        cout<<"We are a string type"<<endl;
    }
    //Elk van BaseType derived datatype moet zijn eigen data kunnen vasthouden, 
    //in dit geval een string:
    string s;
};

//Compleet analoog aan bovenstaande StringType class:
class DoubleType : public BaseType<DoubleType>{
public:
    DoubleType(double _d){
        set(_d);
    }
    double get(){
        return d;
    }
    void set(double _d){
        d=_d;
    }
    static void getTypes(){
        cout<<"We are a double type"<<endl;
    }
    double d;
};

//Deze class-template specialisations linken de 'derived' typedef (i.e DoubleType) 
//aan het originele type (de template variable, i.e double)
template <>
class BaseType<double>{
public:
    typedef DoubleType derived;
};
//Analoog aan bovenstaand
template <>
class BaseType<string>{
public:
    typedef StringType derived;
};

//Deze class heeft get() methods die een derived type (DoubleType of StringType) 
//terug geven voor een C++ datatype, indien een conversie bestaat.
//Er is een implementatie voor zowel double en string als 
//voor vector<double> en vector<string>
class DatatypeFactory{
public:
    //Converteer een I=(double,string) 
    //naar BaseType<I>::derived=(DoubleType,StringType)
    template <class I>
    static typename BaseType<I>::derived get(I &s){
        return typename BaseType<I>::derived(s);
    }
    //Converteer een vector<I>=(vector<double>,vector<string>) 
    //naar vector<BaseType<I>::derived>=(vector<DoubleType>,vector<StringType>)
    template <class I>
    static vector<typename BaseType<I>::derived> get(vector<I> &vd){

        vector<typename BaseType<I>::derived> vo;
        for(typename vector<I>::iterator it=vd.begin();it!=vd.end();++it){
            vo.push_back(typename BaseType<I>::derived(*it));
        }
        return vo;
    }
};

//De base class voor een output Writer
template <class Child>
class Writer{
public:
    template <class S>
    void write(S d)
    {
        static_cast<Child*>(this)->write(d);
    }
};
//Implementatie van een Writer:
class SomeWriter : public Writer<SomeWriter > {
public:
    template <class S>
    void write(S d){
        //De DatatypeFactory::get(d) retourneerd,
        // afhankelijk van de input een geconverteerd datatype:
        // double d --> DoubleType
        // string d --> StringType
        // vector<double d> --> vector<DoubleType>
        // vector<string d> --> vector<StringType>      
        write_data(DatatypeFactory::get(d));
    }

    //Cout een object van type BaseType<>
    template <class S>
    void write_data(S d)
    {
        //Note, alle implementaties van BaseType hebben een methode getTypes().
        //deze methode print de naam van het datatype
        cout<<"H Writing an : "<<endl;
        S::getTypes();
        
        //Ook heeft elke implementatie van BaseType een get(), 
        //welke de waarde terug geeft.
        cout<<"H: "<<d.get()<<endl;
    }
    
    //Print een vector van BaseType<>'s
    template <class S>
    void write_data(vector<S> d)
    {
        cout<<"H Writing a vector of: "<<endl;
        S::getTypes();
        for(typename vector<S>::iterator it=d.begin(); it!=d.end(); ++it){
            cout<<"H Writing: "<<(*it).get()<<endl;
        }
    }

};

int main (void)
{
    SomeWriter s;
    vector<double> vd(3);
    vd[0]=2.2;
    vd[1]=2.1;
    vd[2]=2.0;
    s.write(vd);
    
    string s="test";
    s.write(s);
}


Nu, ik ben mij er van bewust dat bovenstaande code niet direct enorm leesbaar is, maar het stelt mij wel in staat om iets elegentas te doen.
In het geval van HDF5 dient elk datatype een HDF5 equivalent te hebben, dus stel dat ik ook int's wil kunnen schrijven, dan hoef ik enkel de BaseType-implementatie te schrijven:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class IntType : public BaseType<IntType>{
public:
    IntType(int _d){
        set(_d);
    }
    int get(){
        return d;
    }
    void set(int _d){
        d=_d;
    }
    static void getTypes(){
        cout<<"We are a int type"<<endl;
    }

    int d;
};
template <>
class BaseType<int>{
public:
    typedef IntType derived;
};

En nu kan ik ook:
C++:
1
2
int i=12;
w.write(i);

doen....
Goed, dat werkt dus, en is m.i erg handig.
Je moet bedenken dat 'users' van de software zelf de code gaan uitbreiden, er komen regelmatig nieuwe classes bij die weggeschreven moeten kunnen worden.
Ik wil dus niet dat die users dan ook methoden moeten toevoegen aan de OutputWriter, enkel het schrijven van een DataType zou genoeg moeten zijn...
Ook het idee van EddoH gaat dus niet echt werken, ik wil juist meerdere outputwriters kunnen definiëren.
Tenslotte zie ik Jewest suggestie ook niet helemaal... alles converteren naar strings?
Bottom line: het werkt dus min of meer, alleen kan ik geen pointer hebben naar verschillende implementaties van Writer, á la:
C++:
1
2
3
4
5
6
Writer* w; 
if(...){ 
    w = SomeWriter(); 
}else{ 
    w = AnotherWriter(); 
}

Allemaal alvast bedankt voor het meedenken, en excuses voor de lappen code :+

  • Infinitive
  • Registratie: Maart 2001
  • Laatst online: 25-09-2023
Onafhankelijk van hoe je het precies implementeerd, wil je per object- en writertype combinatie het schrijfgedrag kunnen beinvloeden, of per object en writer-type onafhankelijk?

In het latere geval heb je een indirectie/abstractie: elk object heeft zijn eigen manier om naar een vast-gedefinieerde writer-interface te schrijven, en afhankelijk van writer-implementatie wordt dat op verschillende manieren weggeschreven. Of dit kan hangt er vanaf of je een data-onafhankelijke writer-interface kan definieren. Dit kan bestaan uit algemene proceduren voor primitieve typen zoals writeInt, writeBoolean, writeString, maar je kunt daar ook hogher-niveau concepten in kwijt zoals iets als beginSection of endSection of wat voor abstracte structuur je output ook maar mag hebben.

putStr $ map (x -> chr $ round $ 21/2 * x^3 - 92 * x^2 + 503/2 * x - 105) [1..4]


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 03:12

.oisyn

Moderator Devschuur®

Demotivational Speaker

DRAFTER86 schreef op donderdag 13 juni 2013 @ 09:33:
Dus stel dat ik een class Particle heb, dan moet ik een HDF5 datatype definiëren waarin ik specificeer welke members van mijn Particle object overeenkomen met welke HDF5-object members.
Ik wil dus niet dat elke keer als er een nieuw datatype bij komt men ook een virtual method aan alle outputwriters moeten toevoegen.
Maar waarom zou de implementatie van een Writer dan moeten weten wat een Particle is? Is een Particle niet gewoon een verzameling van z'n eigenschappen en een typenaam? En dan heb je uiteindelijk alleen een interface nodig voor fundamentele datatypen, en een manier om die te groeperen in custom datatypen.

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
55
56
57
58
59
60
61
62
class IWriter
{
public:
    virtual void Write(int i) = 0;
    virtual void Write(float f) = 0;
    // ...
    virtual void BeginType(const std::string & typeName) = 0;
    virtual void EndType() = 0;
};

class Serializer
{
public:
    Serializer(IWriter * pWriter) : m_pWriter(pWriter);

    IWriter * GetWriter() { return m_pWriter; }
    void BeginType(const std::string & typeName) { m_pWriter->BeginType(typeName); }
    void EndType() { m_pWriter->EndType(); }

private:
    IWriter * m_pWriter;
};

Serializer& operator<<(Serializer & s, int i)
{
    s.GetWriter()->Write(i);
    return s;
}

Serializer& operator<<(Serializer & s, float f)
{
    s.GetWriter()->Write(f);
    return s;
}

////////////////////////////
// User code
class Particle
{
public:
    Particle() { /* whatever */ }

private:
    int size;
    float speed;

    friend Serializer& operator<<(Serializer & s, const Particle & p)
    {
        s.BeginType("Particle");
        s << size << speed;
        s.EndType();
        return s;
    }
};

void Foo()
{
    IWiter * pWriter = toXml ? new XmlWriter() : new JsonWriter();
    Serializer s(pWriter);
    Particle p;
    s << p;
}

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.


  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Dat is inderdaad vergelijkbaar met een van de mogelijkheden in het .NET framework

( MSDN: ISerializable Interface (System.Runtime.Serialization) )

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


  • DRAFTER86
  • Registratie: April 2002
  • Laatst online: 19:31
.oisyn schreef op donderdag 13 juni 2013 @ 11:14:
[...]

Maar waarom zou de implementatie van een Writer dan moeten weten wat een Particle is? Is een Particle niet gewoon een verzameling van z'n eigenschappen en een typenaam? En dan heb je uiteindelijk alleen een interface nodig voor fundamentele datatypen, en een manier om die te groeperen in custom datatypen.

C++:
1
knip
Maar ben ik dan niet mijn eigen dataformaat aan het schrijven? Ik zie niet zo goed wat het toevoegen van de abstractielaag Serializer toevoegt...
Remember, ik wil mijn objecten niet perse naar een string() converten, het mooie van (in dit geval) HDF5 is juist dat je makkelijk array's van structs (Ik gebruik de C-bindings) kunt schrijven, mits je een HDF5-datatype voor die struct bouwt...
Dus stel ik heb een vector met Particles (en dat kunnen er miljoenen zijn), dan kan ik die in één keer aan de HDF5 library voeren, gegeven de begin() pointer en het bijbehorende HDF5 dataype (welke ik zelf moet maken).
Het HDF5 datatype is dan gewoon een object dat beschrijft welke HDF5 variabelen op welke memory-offset in de struct staan.
Dus inderdaad, een Writer hoeft niet te weten 'wat' en Particle is, zolang de DatatypeFactory maar een conversie kan doen van Particle naar een HDF5Particle type, waarin bovengenoemde offset's gedefinieerd zijn.
Mocht het niet duidelijk zijn, vraag dan alsjeblieft, ik ben mij er terdege van bewust dat mijn uitleg wat warrig is :)
En nogmaal, de code doet opzich wat hij moet doen, en biedt de gewenste flexibiliteit, maar als bij-effect van het CRT Pattern kan ik geen heterogene pointer maken:
C++:
1
Writer* w = SomeWriter();

Aangezien dan de template-var natuurlijk onbekend is...

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 03:12

.oisyn

Moderator Devschuur®

Demotivational Speaker

DRAFTER86 schreef op donderdag 13 juni 2013 @ 11:48:
[...]


Maar ben ik dan niet mijn eigen dataformaat aan het schrijven?
Nee, je bent een serializatielaag aan het schrijven. Het dataformaat hangt af van de implementatie van een IWriter in mijn voorbeeld.
Ik zie niet zo goed wat het toevoegen van de abstractielaag Serializer toevoegt...
In dit voorbeeld niets, maar vergelijk het met std::iostream en std::streambuf. De streambuf is de interface die geïmplementeerd wordt, de iostream is gewoon het object waarmee de code interacteert. Daar kan dus allerhande functionaliteit in gestopt worden, zoals bijvoorbeeld ondersteuning voor containers die automatisch de contents serializeren.

Ja, feitelijk kun je alle functionaliteit van Serializer ook in IWriter kwijt, maar dat is mijns inziens niet zo'n nette opzet omdat je al die functionaliteit dan ook inherit in elke derived class van IWriter.
Remember, ik wil mijn objecten niet perse naar een string() converten
Dat gebeurt in mijn code toch ook niet?
Dus inderdaad, een Writer hoeft niet te weten 'wat' en Particle is, zolang de DatatypeFactory maar een conversie kan doen van Particle naar een HDF5Particle type, waarin bovengenoemde offset's gedefinieerd zijn.
Het probleem waar jij tegenaanloopt is dat een Writer weldegelijk toegang moet hebben tot het type Particle tijdens compiletime, en daarom kun je de boel niet typeloos abstraheren. Maar als je het zo omschrijft, kun je die conversie niet gewoon datadriven doen met behulp van de typeinfo van het type? Een std::map<std::typeinfo, HDFType*> als het ware

(implementatiedetail: std::typeinfo heeft geen public copy ctor dus je zult moeten werken met references of pointers ernaar, wat betekent dat je het in een wrapper object moet gieten om 'm in een std::map te kunnen stoppen - het type zelf is wel geordend en heeft een hashcode, dus het is relatief simpel om een wrapper te maken voor containment in een std::map of een std::unordered_map).

[ Voor 5% gewijzigd door .oisyn op 13-06-2013 12:01 ]

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.


  • DRAFTER86
  • Registratie: April 2002
  • Laatst online: 19:31
.oisyn schreef op donderdag 13 juni 2013 @ 11:58:
[...]

Nee, je bent een serializatielaag aan het schrijven. Het dataformaat hangt af van de implementatie van een IWriter in mijn voorbeeld.


[...]

In dit voorbeeld niets, maar vergelijk het met std::iostream en std::streambuf. De streambuf is de interface die geïmplementeerd wordt, de iostream is gewoon het object waarmee de code interacteert. Daar kan dus allerhande functionaliteit in gestopt worden, zoals bijvoorbeeld ondersteuning voor containers die automatisch de contents serializeren.

Ja, feitelijk kun je alle functionaliteit van Serializer ook in IWriter kwijt, maar dat is mijns inziens niet zo'n nette opzet omdat je al die functionaliteit dan ook inherit in elke derived class van IWriter.
Maar dit is nu juist de functionaliteit die HDF5 voor zijn rekening neemt, ik hoef niet na te denken over hoe ik mijn C++ objecten serialiseer, ik hoef enkel voor de benodigde datatype-conversies (C++ object -> HDF5 Object) te zorgen.
Dit werkt ook gewoon in mijn (misschien wat onconventionele) oplossing (zie reactie hieronder).
Dat gebeurt in mijn code toch ook niet?
Correct, my bad.
Het probleem waar jij tegenaanloopt is dat een Writer weldegelijk toegang moet hebben tot het type Particle tijdens compiletime, en daarom kun je de boel niet typeloos abstraheren. Maar als je het zo omschrijft, kun je die conversie niet gewoon datadriven doen met behulp van de typeinfo van het type? Een std::map<std::typeinfo, HDFType*> als het ware

(implementatiedetail: std::typeinfo heeft geen public copy ctor dus je zult moeten werken met references of pointers ernaar, wat betekent dat je het in een wrapper object moet gieten om 'm in een std::map te kunnen stoppen - het type zelf is wel geordend en heeft een hashcode, dus het is relatief simpel om een wrapper te maken voor containment in een std::map of een std::unordered_map).
Klopt, het koppelen van C++ objecten (Particle of double) aan in dit geval HDF5 datatypes (DoubleType, ParticleType) is nodig, maar dit heb ik naar mijn idee al opgelost:
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
class DoubleType : public BaseType<DoubleType>{
public:
    DoubleType(double _d){
        set(_d);
    }
    double get(){
        return d;
    }
    void set(double _d){
        d=_d;
    }
    static void getTypes(){
        cout<<"We are a double type"<<endl;
    }
    double d;
};

template <>
class BaseType<double>{
public:
    typedef DoubleType derived;
};
class DatatypeFactory{
public:
    //Converteer een I=(double,string) 
    //naar BaseType<I>::derived=(DoubleType,StringType)
    template <class I>
    static typename BaseType<I>::derived get(I &s){
        return typename BaseType<I>::derived(s);
    }
}

Dus als ik DatatypeFactory::get(double(1.1)) doe, krijg ik een DoubleType met DoubleType.d=1.1 terug.
Vervolgens geeft DoubleType::getTypes() (elk datatype heeft de getTypes()) me bijvoorbeeld het HDF5 Datatype...
Hierdoor kan een HDF5 Writer bijvoorbeeld gewoon:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SomeWriter : public Writer<SomeWriter > {
public:
    template <class S>
    void write(S d){
        write_hdf5(DatatypeFactory::get(d));
    }

    template <class S>
    void write_hdf5(S d)
    {
        //Pseudo call om een HDF5 object te schrijven
        H5Write(S::getTypes(),&d);
    }
};

Doen. Dus de Writer weet niks van DoubleType's, etc, enkel dat hij alleen maar objecten krijgt welke dmv de DatatypeFactory een HDF5 equivalent hebben.
Dus de een ParticleType::getTypes() geeft het HDF5 datatype van een ParticleType object, niet die van een Particle...
Goed, ik hoop dat dit mijn bedoeling wat verduidelijkt, het valt me niet mee om dit soort dingen helder op te schrijven :)

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 03:12

.oisyn

Moderator Devschuur®

Demotivational Speaker

Je omschrijft steeds wat je hebt maar niet wat je probleem nou is. Maar als ik het goed begrijp is je probleem dat SomeWriter niet te upcasten is naar een generieke interface want dan verlies je essentiele typeinformatie. De enige reden waarom je die typeinformatie nodig hebt is omdat je de conversie @ compiletime implementeert dmv template specialization, maar mijn suggestie is om dat runtime te doen met een geassocieerde container zoals een std::map of een std::unordered_map.

Maar eerst even een comment over je design van BaseType, want die vind ik eerlijk gezegd een beetje raar. Waarom derived DoubleType van BaseType<DoubleType>? Dat heeft toch verder geen enkel praktisch nut? Daarnaast vind ik het vreemd dat je voor de conversie van native type naar het juiste BaseType ook via BaseType laat gaan. Als ik jouw opzet zou gebruiken dan zou ik het zo hebben aangepakt:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
class BaseType
{
    virtual void getTypes() = 0;
};

class DoubleType : BaseType
{
    // ...
    virtual void getTypes() { /* ... */ }
}

template<class> struct NativeToBaseType;
template<> struct NativeToBaseType<double> { typedef DoubleType derived; }

Het voordeel is nu dat BaseType wel een echte interface is en code ook iets kan als ze alleen een referentie hebben naar BaseType zonder het concrete type te weten (en dus de mogelijkheid om op een abstracte manier HDF5 metadata van dat type op te vragen).

Goed, dan nu een tool om zo'n basetype te instantieren.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// abstracte factory voor je types
class TypeFactory
{
public:
    virtual BaseType * Create(const void * data) = 0;
};

// concrete factory voor allerhande types. Kan via templates of custom implementaties.
template<class T>
class ConcreteTypeFactory : TypeFactory
{
public:
    virtual BaseType * Create(const void * data)
    {
        return new typename NativeToBaseType<T>::derived(*static_cast<const T*>(data));
    }
};

Nu de factory geabstraheerd is, heb je alleen nog een tool nodig dat de juiste factory opzoekt aan de hand van een type. Als de code het type weet kan hij natuurlijk gewoon een ConcreteTypeFactory<T> pakken, maar het idee is nou juist om die type dependency weg te halen. Wat je feitelijk nodig hebt is een database die aan de hand van een abstracte key die het type representeert (std::typeinfo) de juiste factory teruggeeft.

C++:
1
2
3
4
TypeFactory * GetFactoryForType(const std::typeinfo & type)
{
    // implementation is left as exercise for reader ;)
}


De guts van die functie zijn verder niet bijzonder interessant; wat ik al zei, iets met een std::map<TypeInfoWrapper, TypeFactory*>. Essentieel hierbij is dat alle ondersteunden types wel tijdens runtime aan de map toegevoegd moeten worden. Dat kan wel enigszins geautomatiseerd bij startup dmv constructors en static instances. NativeToBaseType<> kan het bijvoorbeeld doen omdat je daar toch een koppeling maakt van alle ondersteunde types.

Wat bereiken we hier nou allemaal mee? Heel simpel, nu kunnen we je probleem oplossen, namelijk dat je altijd het concrete type van een Writer moet weten. We kunnen de interface nu zo definieren:
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
class Writer
{
public:
    template<class T>
    void write(const T & t)
    {
        write(&t, typeid(t));
    }

protected:
    virtual void write(const void * data, const std::typeinfo & info) = 0;
};

class SomeWriter : public Writer
{
protected:
    virtual void write(const void * data, const std::typeinfo & info)
    {
        TypeFactory * pFactory = GetFactoryForType(info);
        BaseType * pType = pFactory->Create(data);
        H5Write(pType->getTypes(), pType);
    }
}

void Foo()
{
    Writer * pWriter = new SomeWriter(); // of XmlWriter of whatever
    pWriter->write(double(3.1415));
    pWriter->write(Particle());
}

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.

Pagina: 1