[OOP/C++] Abstractie voor patroon fields in class hierarchy?

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • RayNbow
  • Registratie: Maart 2003
  • Laatst online: 19:59

RayNbow

Kirika <3

Topicstarter
Voor een bepaald project ben ik bezig met het ontwerpen van de klassestructuur. Zonder al te diep op details in te gaan, hebben we in dit project een aantal concepten en bij elk concept een afgeleid concept. Stel we hebben het concept van Foo met onderstaande eigenschappen
C++:
1
2
3
4
5
6
7
8
class Foo {
public:
    Foo(double X, double Y, double Z) : X(X), Y(Y), Z(Z) {}

    double X;
    double Y;
    double Z;
};

dan hebben we ook een bijbehorend concept AdjustedFoo:
C++:
1
2
3
4
5
6
7
8
9
10
11
class AdjustedFoo : public Foo {
public:
    AdjustedFoo(double X, double Y, double Z, double w_X, double w_Y, double w_Z, Foo* original = nullptr)
        : Foo(X, Y, Z), w_Z(w_Z), original(original) {}

    double w_X;
    double w_Y;
    double w_Z;

    Foo* original;
};

Met andere woorden, een AdjustedFoo heeft naast de velden die Foo ook heeft (nl. X, Y en Z) de velden w_X, w_Y en w_Z plus een verwijzing naar een Foo.

Deze relatie tussen Foo en AdjustedFoo is in het project niet een eenmalig iets, maar een terugkerend patroon. Als we bijv. een Bar hebben met velden ω, φ en κ, dan hebben we ook een AdjustedBar met velden ω, φ, κ, w_ω, w_φ, w_κ en original.

Nu is mijn vraag, bestaat er in C++ een abstractie voor dit patroon? Of zit er niets anders op dan alles uit te tikken? Of loont het om bijv. een script te schrijven die tijdens het buildproces de code voor de Adjusted-klassen genereert (met mogelijk als nadeel dat de IDE dan niet altijd op de hoogte is van deze gegenereerde klassen en geen ondersteuning kan bieden)? Of zie ik andere opties over het hoofd?


(optionele bijlage: proof of concept in Python)

Als ik deze vraag trouwens zelf zou moeten beantwoorden in de context van Python, dan zou ik wat boilerplate kunnen schrijven waarmee ik vervolgens Foo en AdjustedFoo als volgt zou kunnen definiëren:
Python:
1
2
3
4
5
6
7
8
9
@data
class Foo:
    X: float
    Y: float
    Z: float

@Adjusted
class AdjustedFoo(Foo):
    pass

Zodat ik deze classes als volgt kan gebruiken:
>>> foo = Foo(1, 2, 3)
>>> adj_foo = AdjustedFoo(1.1, 1.9, 3.01, 0.1, 0.2, 0.3, foo)
>>> print(foo)
Foo(X = 1, Y = 2, Z = 3)
>>> print(adj_foo)
AdjustedFoo(X = 1.1, Y = 1.9, Z = 3.01, w_X = 0.1, w_Y = 0.2, w_Z = 0.3, original = Foo(X = 1, Y = 2, Z = 3))
(niet zo robuuste boilerplate om bovenstaande Python-snippet aan de praat te krijgen)
Python:
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
def data(cls):
    annotations = {}
    for ancestor in cls.mro()[::-1]:
        if hasattr(ancestor, '__is_data__') or ancestor == cls:
            annotations.update(ancestor.__annotations__)
    
    fields = list(annotations.keys())
    name = cls.__name__
    bases = cls.__bases__
    
    def init(self, *args, **kwargs):
        if len(args) > len(fields):
            raise TypeError('__init__() takes {} positional arguments but {} were given'.format(len(fields), len(args)))
        else:
            for i, arg in enumerate(args):
                field_name = fields[i]
                if field_name in kwargs:
                    raise TypeError("__init__() got multiple values for argument '{}'".format(field_name))
                else:
                    kwargs[field_name] = arg
    
            if len(kwargs) < len(fields):
                missing_args = ["'{}'".format(field_name) for field_name in fields if field_name not in kwargs]
                raise TypeError("__init__() missing {} required argument(s): {}".format(len(missing_args), ', '.join(missing_args)))
            
            for k, v in kwargs.items():
                setattr(self, k, v)
    
    def representation(self):
        return '{}({})'.format(name, ', '.join(
            '{} = {!r}'.format(field_name, getattr(self, field_name))
            for field_name in fields
        ))
    
    return type(name, bases, {
        '__annotations__': cls.__annotations__,
        '__is_data__': True,
        '__init__': init,
        '__repr__': representation
    })

def Adjusted(cls):
    annotations = {}
    for ancestor in cls.mro()[::-1]:
        if hasattr(ancestor, '__is_data__'):
            annotations.update(ancestor.__annotations__)
    
    for key in annotations.keys():
        if key == 'original':
            continue
        
        w_key = 'w_{}'.format(key)
        cls.__annotations__[w_key] = annotations[key]
    
    bases = cls.__bases__
    if len(bases) != 1 or not hasattr(bases[0], '__is_data__'):
        raise TypeError('This PoC of @Adjusted only works for classes that derive from a single @data class')
    
    cls.__annotations__['original'] = bases[0]
    
    return data(cls)
Hierbij gebruik ik dus de dynamische eigenschappen van Python om de definitie van een class te kunnen inspecteren en daarmee op dynamische wijze een bijhorende class te genereren.

Ipsa Scientia Potestas Est
NNID: ShinNoNoir


Acties:
  • +1 Henk 'm!

  • EddoH
  • Registratie: Maart 2009
  • Niet online

EddoH

Backpfeifengesicht

Onderweg en geen tijd om het uit te denken, dus just throwing this out there:
Een template class die T extend met 2 members, <T*> original, en <T> w_

Acties:
  • 0 Henk 'm!

  • MartenBE
  • Registratie: December 2012
  • Laatst online: 10-09 18:09
Aangezien Foo en AdjustedFoo eigenlijk vrij gelijkaardig zijn, is het geen idee om de dezelfde klasse te gebruiken voor zowel de Foo als de aangepaste Foo' s?

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo 
{
public:
    Foo(double X, double Y, double Z) : X{X}, Y{Y}, Z{Z} 
    {
    }

    double X;
    double Y;
    double Z;

    std::vector<Foo> adjustments;
};


Zo is de link tussen de foo en de adjustments altijd duidelijk en heb je slechts 1 klasse nodig. Je kan de vector ook private maken en dan add methodes toevoegen indien je dat wil. Je kan ook de vector veranderen naar std::vector<const Foo*> enzo.

Acties:
  • 0 Henk 'm!

  • RayNbow
  • Registratie: Maart 2003
  • Laatst online: 19:59

RayNbow

Kirika <3

Topicstarter
EddoH schreef op dinsdag 5 februari 2019 @ 22:29:
Onderweg en geen tijd om het uit te denken, dus just throwing this out there:
Een template class die T extend met 2 members, <T*> original, en <T> w_
Ik werd vanochtend wakker met een vergelijkbaar idee. Ik zou inderdaad eens kunnen kijken of ik dit netjes kan uitschrijven.
MartenBE schreef op dinsdag 5 februari 2019 @ 23:05:
Aangezien Foo en AdjustedFoo eigenlijk vrij gelijkaardig zijn, is het geen idee om de dezelfde klasse te gebruiken voor zowel de Foo als de aangepaste Foo' s?

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo 
{
public:
    Foo(double X, double Y, double Z) : X{X}, Y{Y}, Z{Z} 
    {
    }

    double X;
    double Y;
    double Z;

    std::vector<Foo> adjustments;
};


Zo is de link tussen de foo en de adjustments altijd duidelijk en heb je slechts 1 klasse nodig. Je kan de vector ook private maken en dan add methodes toevoegen indien je dat wil. Je kan ook de vector veranderen naar std::vector<const Foo*> enzo.
Op zich geen verkeerd idee om een vector te gebruiken met daarin een opvolgende vereffende Foo-instanties. Dit legt dan alleen wel min of meer vast dat van elke Foo er maar 1 vereffende variant bestaat (of dit een grote beperking is ben ik nog niet uit). In jouw voorstel missen alleen nog wel de w_*-velden. Dit zou kunnen worden opgelost door iets als std::vector<Foo> ws toe te voegen, behalve dat deze w-waarden zelf niet een Foo voorstellen.

Ipsa Scientia Potestas Est
NNID: ShinNoNoir


Acties:
  • 0 Henk 'm!

  • ThomasG
  • Registratie: Juni 2006
  • Laatst online: 23:25
Volgens mij kun je het het beste terug dringen tot een pseudo-decorator pattern. Je maakt een AdjustedFoo die dezelfde interfacing heeft als Foo en intern bestaat uit twee Foo objecten (original, en adjusted). De getX methode van AdjustedFoo doet dan bijvoorbeeld: return original.getX() + adjusted.getX(). Iets als:
C++:
1
2
3
4
5
6
7
8
9
10
11
class IFoo {
// ...
}

class Foo : IFoo {
}

class AdjustedFoo : IFoo {
    IFoo original;
    IFoo adjusted;
}


Je zou dat nog kunnen verbeteren met templates en specializatie.

Acties:
  • 0 Henk 'm!

  • RayNbow
  • Registratie: Maart 2003
  • Laatst online: 19:59

RayNbow

Kirika <3

Topicstarter
Vanavond heb ik wat lopen prutsen op een idee dat ik vanochtend had en wat @EddoH eerder in deze draad aanstipte.

Laten we beginnen met het abstraheren van velden dat een class kan hebben:
C++:
1
2
3
4
template <typename T>
struct FieldsOf {
    using type = void;
};
En een alias om ons wat typewerk te besparen:
C++:
1
2
template <typename T>
using fields_of = typename FieldsOf<T>::type;


Het adjusted-patroon kan dan als worden vastgelegd:
C++:
1
2
3
4
5
6
template <typename T>
class Adjusted : public T {
public:
    fields_of<T> w;
    T* original;
};


Toegepast op een Foo:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo;

template <>
struct FieldsOf<Foo> {
    struct type {
        double X;
        double Y;
        double Z;
    };
};

class Foo : public fields_of<Foo> {};
using AdjustedFoo = Adjusted<Foo>;

Nu beschikken we over een Foo met velden X, Y en Z en over een AdjustedFoo met velden X, Y, Z, w.X, w.Y, w.Z en original...

...we missen alleen nog een fatsoenlijke constructor. 🤔

Ipsa Scientia Potestas Est
NNID: ShinNoNoir


Acties:
  • 0 Henk 'm!

  • EddoH
  • Registratie: Maart 2009
  • Niet online

EddoH

Backpfeifengesicht

Nice! Die constructor kwam ik in mijn gedachte ook niet helemaal uit nog....

Acties:
  • 0 Henk 'm!

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 00:05
De "FieldsOf" doet me denken dat je std::tuple<> nog niet kent. Dat is misschien wat veel retrofit-werk op je bestaande code, dus ik wil niet claimen dat het voor jouw bestaande code winst oplevert.

Wat me wel verbaast is dat AdjustedFoo afgeleid is van Foo. Dat betekent dat je in een AdjustedFoo dus een Foo::x member hebt, een AdjustedFoo::w_x, én een original->x. Dat zijn dus 3 verschillende x'en. Is dat er niet 1 te veel?

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


Acties:
  • 0 Henk 'm!

  • RayNbow
  • Registratie: Maart 2003
  • Laatst online: 19:59

RayNbow

Kirika <3

Topicstarter
MSalters schreef op donderdag 7 februari 2019 @ 10:01:
De "FieldsOf" doet me denken dat je std::tuple<> nog niet kent.
Ik weet dat tuple<> bestaat, maar ik zie niet zo snel hoe ik dit hier kan gebruiken.
Dat is misschien wat veel retrofit-werk op je bestaande code, dus ik wil niet claimen dat het voor jouw bestaande code winst oplevert.
Het mooie is, er is nog weinig bestaande code, dus het goedkoopste moment om ontwerpbeslissingen te nemen is nu. :)
Wat me wel verbaast is dat AdjustedFoo afgeleid is van Foo. Dat betekent dat je in een AdjustedFoo dus een Foo::x member hebt, een AdjustedFoo::w_x, én een original->x. Dat zijn dus 3 verschillende x'en.
Het zijn eigenlijk 2 X-en die je kunt benaderen, namelijk X en original->X. Het veld w_X (of w.X in het template-probeersel) is een afgeleide toetsingswaarde.

Ipsa Scientia Potestas Est
NNID: ShinNoNoir


Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 12-09 15:22

.oisyn

Moderator Devschuur®

Demotivational Speaker

MSalters schreef op donderdag 7 februari 2019 @ 10:01:
De "FieldsOf" doet me denken dat je std::tuple<> nog niet kent. Dat is misschien wat veel retrofit-werk op je bestaande code, dus ik wil niet claimen dat het voor jouw bestaande code winst oplevert.
Het nadeel van tuples is dat de members geen naam hebben. Het werken ermee in deze specifieke context lijkt me erg onhandig.

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.


Acties:
  • 0 Henk 'm!

  • ThomasG
  • Registratie: Juni 2006
  • Laatst online: 23:25
Als Foo, Bar e.d. enkel eenvoudige PODs zijn kan het zonder een FieldsOf, door het type gewoon te hergebruiken. De constructor is dan ook niet heel ingewikkeld (moet wel e.a. aangepast worden als je pointers/references e.d. wilt):

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
#include <iostream>

template<typename _Type>
struct Adjusted : public _Type {
    _Type w;

    Adjusted(_Type original, _Type adjusted)
        : _Type{original}, w{adjusted}
    { };
};

struct Foo {
    int x;
    int y;
    int z;
};

typedef Adjusted<Foo> AdjustedFoo;

int main(void) {
    AdjustedFoo af {{1, 2, 3}, {4, 5, 6}};

    std::cout << af.x << std::endl;
    std::cout << af.w.x << std::endl;
    return 0;
}

Acties:
  • 0 Henk 'm!

  • Olaf van der Spek
  • Registratie: September 2000
  • Niet online
RayNbow schreef op dinsdag 5 februari 2019 @ 21:32:
Met andere woorden, een AdjustedFoo heeft naast de velden die Foo ook heeft (nl. X, Y en Z) de velden w_X, w_Y en w_Z plus een verwijzing naar een Foo.
AdjustedFoo is al een Foo (inheritance), waar verwijst original naar?

Een concreet voorbeeld zou kunnen helpen.

[ Voor 5% gewijzigd door Olaf van der Spek op 11-02-2019 15:15 ]


Acties:
  • 0 Henk 'm!

  • RayNbow
  • Registratie: Maart 2003
  • Laatst online: 19:59

RayNbow

Kirika <3

Topicstarter
Olaf van der Spek schreef op maandag 11 februari 2019 @ 15:13:
[...]

AdjustedFoo is al een Foo (inheritance), waar verwijst original naar?
Naar de Foo voor de vereffening (adjustment). :p
Een concreet voorbeeld zou kunnen helpen.
Foo is een of andere waarneming (of: meting) van een of andere onbekende. Gezien in het voorbeeld er een X, Y en Z zijn, is Foo een of andere positiebepaling in een driedimensionale ruimte. N.B.: Ik had ook kunnen kiezen voor een eendimensionaal voorbeeld en in dat geval had Foo slechts een enkel veld gehad.

Als we nu een hele verzameling samenhangende (waarbij de definitie van samenhang afhangt van het gekozen model) waarnemingen hebben (deze waarnemingen hoeven niet van hetzelfde type te zijn), dan is het mogelijk om deze waarnemingen te vereffenen. Uit een vereffening komen vereffende (adjusted) waarnemingen rollen. Een vereffende waarneming is een aanpassing van de oorspronkelijke waarneming, zodanig dat deze beter past als je alle andere (vereffende) waarnemingen in acht neemt. Anders gezegd, je zou vereffenen ook kunnen zien als dingen passend maken.

Nu terug naar Foo en AdjustedFoo: Stel we hebben van een of ander iets een positie weten te meten. Laten we zeggen dat uit onze meting p=(1.0, 2.0, 3.0) komt rollen. Nu hebben we ook allerlei andere samenhangende metingen verricht en we gooien alles in een of ander vereffeningspakket. Stel dat we nu een vereffende waarneming krijgen, bijv. p̂=(1.1, 1.9, 3.01). Als we de statistische toetsing achterwege laten, dan hebben we nu het voorbeeld uit het stukje Python in de TS.


Om het idee van vereffenen zelf iets concreter te maken, volgt hier een versimpeld voorbeeld. Stel je hebt een vierkant (en je mag aannemen dat dit een vierkant is). Je meet vervolgens alle vier de zijden. Hieruit komen de volgende metingen:
4.04 cm
3.95 cm
4.01 cm
7.12 cm

Hoe lang is waarschijnlijk elke zijde van dit vierkant?

Ipsa Scientia Potestas Est
NNID: ShinNoNoir


Acties:
  • 0 Henk 'm!

  • Olaf van der Spek
  • Registratie: September 2000
  • Niet online
Dus X = original->X + w_x in AdjustedFoo? Is die redundancy nodig?
MSalters in "[OOP/C++] Abstractie voor patroon fields in class hierarchy?" dus eigenlijk..

Denk niet dat het momenteel in C++ simpeler kan.
Met https://herbsutter.com/20...thoughts-on-generative-c/ in de toekomst misschien wel.

Acties:
  • 0 Henk 'm!

  • ThomasG
  • Registratie: Juni 2006
  • Laatst online: 23:25
RayNbow schreef op maandag 11 februari 2019 @ 17:58:
[...]

Naar de Foo voor de vereffening (adjustment). :p

[...]

Foo is een of andere waarneming (of: meting) van een of andere onbekende. Gezien in het voorbeeld er een X, Y en Z zijn, is Foo een of andere positiebepaling in een driedimensionale ruimte. N.B.: Ik had ook kunnen kiezen voor een eendimensionaal voorbeeld en in dat geval had Foo slechts een enkel veld gehad.

Als we nu een hele verzameling samenhangende (waarbij de definitie van samenhang afhangt van het gekozen model) waarnemingen hebben (deze waarnemingen hoeven niet van hetzelfde type te zijn), dan is het mogelijk om deze waarnemingen te vereffenen. Uit een vereffening komen vereffende (adjusted) waarnemingen rollen. Een vereffende waarneming is een aanpassing van de oorspronkelijke waarneming, zodanig dat deze beter past als je alle andere (vereffende) waarnemingen in acht neemt. Anders gezegd, je zou vereffenen ook kunnen zien als dingen passend maken.

Nu terug naar Foo en AdjustedFoo: Stel we hebben van een of ander iets een positie weten te meten. Laten we zeggen dat uit onze meting p=(1.0, 2.0, 3.0) komt rollen. Nu hebben we ook allerlei andere samenhangende metingen verricht en we gooien alles in een of ander vereffeningspakket. Stel dat we nu een vereffende waarneming krijgen, bijv. p̂=(1.1, 1.9, 3.01). Als we de statistische toetsing achterwege laten, dan hebben we nu het voorbeeld uit het stukje Python in de TS.


Om het idee van vereffenen zelf iets concreter te maken, volgt hier een versimpeld voorbeeld. Stel je hebt een vierkant (en je mag aannemen dat dit een vierkant is). Je meet vervolgens alle vier de zijden. Hieruit komen de volgende metingen:
4.04 cm
3.95 cm
4.01 cm
7.12 cm

Hoe lang is waarschijnlijk elke zijde van dit vierkant?
Als ik dus zo lees krijg ik in de indruk dat je helemaal geen AdjustedFoo wilt, of beter gezegd, nodig hebt. Waar je naar zoekt is een varian van Moving average. Daar kun je met wat knutselen bijvoorbeeld Boost.Accumulators voor gebruiken.

Acties:
  • 0 Henk 'm!

  • RayNbow
  • Registratie: Maart 2003
  • Laatst online: 19:59

RayNbow

Kirika <3

Topicstarter
Nee, die w_x is een bepaalde statistische toets (die toetsingswaarde hoort onder een bepaalde drempelwaarde te liggen; zo niet, dan kun je de waarneming als een blunder beschouwen). X = original->X + e(rror).
Denk niet dat het momenteel in C++ simpeler kan.
Met https://herbsutter.com/20...thoughts-on-generative-c/ in de toekomst misschien wel.
Ja, ik heb metaclasses voorbij zien komen. Maar als ik iets zie als C++20 ben ik bang dat ik tot 2030 mag wachten. :p
ThomasG schreef op maandag 11 februari 2019 @ 19:12:
[...]
Als ik dus zo lees krijg ik in de indruk dat je helemaal geen AdjustedFoo wilt, of beter gezegd, nodig hebt. Waar je naar zoekt is een varian van Moving average. Daar kun je met wat knutselen bijvoorbeeld Boost.Accumulators voor gebruiken.
Een moving average is een filter voor een signaal; daar ben ik niet naar op zoek. :) Ik heb namelijk geen tijdreeks.

Misschien dat ik de context waarin ik waarnemingen heb moet visualiseren:
Afbeeldingslocatie: https://i.imgur.com/uWVxbFo.jpg
(Ergens van 't internet geplukt.)

Ik heb te maken met 2D-waarnemingen (van zogenoemde tie points) op een sensorvlak van een fotocamera en met 3D-waarnemingen in het terrein (zogenoemde GCP's). (Die fotocamera hoeft trouwens niet in een vliegtuig te zitten. Voor mijn part is het een smartphone in de handen van iemand op de grond.)

[ Voor 3% gewijzigd door RayNbow op 11-02-2019 19:35 ]

Ipsa Scientia Potestas Est
NNID: ShinNoNoir


Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 12-09 15:22

.oisyn

Moderator Devschuur®

Demotivational Speaker

Olaf van der Spek schreef op maandag 11 februari 2019 @ 18:50:
Dus X = original->X + w_x in AdjustedFoo? Is die redundancy nodig?
Hoe is dat relevant voor het probleem?

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