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

[C++] (Uitgebreide) vector template 'dilemma'

Pagina: 1
Acties:

  • ThomasG
  • Registratie: Juni 2006
  • Laatst online: 23-11 18:54
Ik ben - als uit de hand gelopen hobby - bezig met het schrijven van een game engine in C++11 (is een project wat een lange tijd gaat duren, dus wil graag op de toekomst voorbereid zijn), met o.a. een uitgebreide math library. Nu kom ik alleen door mijn eigen perfectionisme/purisme in de knoop, en heb ik het volgende dilemma:

Ik gebruik meerdere vector dimensies, (2d, 3d, 4d, etc), welke van verschillende type kunnen zijn (float, int, etc). Deze delen allemaal het zelfde interne mechaisme. Ik heb hiervoor één template gemaakt,
welke vervolgens wordt getypedef'd voor de juiste situatie. Alleen wil ik het ook kunnen gebruiken via public properties, dus vector.x, vector.y, etc. in plaats van vector [0], vector [1], etc. Maar intern sla ik alles op in een array met de grote van de dimensie van de vector.

Ik heb twee oplossingen bedacht, maar ik kom er zelf niet goed uit welke oplossing de 'beste' is.

C++: Oplossing 1
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
template <typename _Component, int _Dimension>
class VectorContainer {
public:
    _Component components [_Dimension];
};

template <typename _Component, int _Dimension, typename _Container = VectorContainer <_Component, _Dimension>>
class Vector : public _Container {
    // knip, bevat o.a. variadic constructors/methods, zoals:
    template <typename ... _Variadic>
    Vector (const _Component x, const _Component y, const _Variadic ... components);
    explicit Vector (const _Component (&components) [_Dimension]);
};

template <typename _Component>
class Vector2Container {
public:
    union {
        _Component components [2];

        struct {
            _Component x;
            _Component y;
        };
    };
};

typedef Vector2 <double, 2, Vector2Container <float>> Vector2d;
typedef Vector2 <float, 2, Vector2Container <float>> Vector2f;

template <typename _Component>
class Vector3Container {
public:
    union {
        _Component components [3];

        struct {
            _Component x;
            _Component y;
            _Component z;
            // .. etc
        };
    };
};

// bijv.
Vector2f vect (5.0f, -1.0f);
vect.x = 1.0f;


C++: Oplossing 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename _Component, int _Dimension>
class VectorBase {
    // knip
};

template <typename _Component>
class Vector2 : public VectorBase <_Component, 2> {
public:
    // inherit constructors

    _Component & x ();
    _Component & y ();
};

typedef Vector2 <float> Vector2f;

// bijv.
Vector2f vect (-9.0f, 5.3f);
vect.x() = 1.0f; // vies?!


Even samengevat komt het er dus op neer:
- Oplossing 1, 'container' class met Union struct, publieke variable.
- Oplossing 2, VectorBase class inherit, en reference methods.

Persoonlijk lijkt oplossing 1 mij het meest flexibele, en meest 'nette' (hoewel ze allebei vrij vies zijn volgens mij.) Ik zou graag jullie mening willen horen, of misschien heb je een beter/nettere oplossing.

[ Voor 0% gewijzigd door Creepy op 30-12-2012 17:21 . Reden: Dilemma is met dubbel m ;) ]


  • Ghannes
  • Registratie: Oktober 2002
  • Laatst online: 18-11 20:07
het lijkt alsof je twee dingen door elkaar haalt:
vector[i] wijst naar het i-de element in je vector, terwijl vector.x de member x representeert.

Daarnaast;

C++:
1
vect.x() = 1.0f; // vies?!
Wat wil je hier precies doen? De returnwaarde van de functie x() overschrijven? No sense

  • ThomasG
  • Registratie: Juni 2006
  • Laatst online: 23-11 18:54
Ghannes schreef op donderdag 27 december 2012 @ 16:10:
het lijkt alsof je twee dingen door elkaar haalt:
vector[i] wijst naar het i-de element in je vector, terwijl vector.x de member x representeert.

Daarnaast;

C++:
1
vect.x() = 1.0f; // vies?!
Wat wil je hier precies doen? De returnwaarde van de functie x() overschrijven? No sense
Wat ik nu heb is gewoon één template class waarin alles zit (dus ook dingen als +=, + etc voor alle denkbare types), en het werkt intern met de components array. Ik wil dit dus niet voor alle vector classes gaan kopieren en plakken, en aanpassen. Want ik gebruik misschien op bepaalde punten wel een Vector8. Het zelfde geld voor matrices (ook één template).

Stel ik heb een twee dimensionale vector en ik wil de x hebben. Kan dat nu "niet". Ik moet dan nu doen in de trand van:
C++:
1
2
3
4
5
6
7
8
Vector2f vec;
float foo = vec[0];
//...
vec[0] = bar;

// of
float foo = vec.getX();
vec.setX(bar);

Maar ik wil iets als:
C++:
1
2
3
4
Vector2f vec;
float foo = vec.x;
//...
vec.x = bar;


Dit kan o.a. door de 'container' class, welke een Union heeft met een array en de x/y/.... (zelfde geheugenruimte, maar op verschillende manieren te benaderen). Of ik kan iets doen als:
C++:
1
2
3
4
5
6
7
8
9
template <typename _Component, int _Dimension>
_Component & Vector <_Component, _Dimension>::x () {
    return this->components [0];
};

// Dit werkt gewoon:
Vector2f vect;
vect.x() = 2.0f;
cout << vect.x(); // 2.0f


Maar dan is volgens mij vies, gevaarlijk, en gewoon slecht programmeren. Het probleem bij dit, en bij de getter/setter is dat ik de template class moet gaan overerven voor de verschillende dimensies, waardoor ik dan ook de o.a. constructors moet inheriten, wat ik eigenlijk niet wil.

Het liefste had ik dit gewoon opgelost met een soort van metaprogramming in de template class, maar dit kan helaas niet, iets van:
C++:
1
2
3
4
5
6
7
8
9
union {
    _Component components [_Dimension];
    if (_Dimension >= 2 && _Dimension <= 4) struct {
        if (_Dimension >= 2) _Component x;
        if (_Dimension >= 2) _Component y;
        if (_Dimension >= 3) _Component z
        if (_Dimension >= 4) _Component w;
    };
};

[ Voor 10% gewijzigd door ThomasG op 27-12-2012 16:25 ]


  • MLM
  • Registratie: Juli 2004
  • Laatst online: 12-03-2023

MLM

aka Zolo

Ghannes schreef op donderdag 27 december 2012 @ 16:10:
het lijkt alsof je twee dingen door elkaar haalt:
vector[i] wijst naar het i-de element in je vector, terwijl vector.x de member x representeert.

Daarnaast;

C++:
1
vect.x() = 1.0f; // vies?!
Wat wil je hier precies doen? De returnwaarde van de functie x() overschrijven? No sense
Dat kan prima, je returned immers een reference. Het ziet er alleen niet fijn uit.

Als je per-se x, y, z wilt kunnen gebruiken, dan moeten dat members zijn. Je kan ook gewoon template specializen en beide technieken pakken
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename _Component, int _Dimension>
union VectorContainer
{
   _Component components[_Dimension];
};

template<typename _Component>
union VectorContainer<_Component, 2>
{
  _Component components[2];
  struct
  {
    _Component x, y;
  };
};

//specialize voor 3 en 4 hier


Kan je toch nog een vector<T, 10> declareren, maar dan heeft ie alleen geen x, y, z :)
Daarnaast wil je waarschijnlijk toch specializen, immers cross-product zit alleen op 3D vectoren :)

Alternatief: recursief vector opbouwen uit x/y/z, met Vec<N-1> als baseclass voor Vec<N>, wel oppassen met memory layout dan :P

Mogelijk heb je wel duplicate code te pakken dan, als je classes complexer worden (Constructors etc). Je kan wellicht bovenstaand als base-class voor een "echte" class pakken zoals in je voorbeeld.

Offtopic: wil je echt een variadic constructor? Wat doe je dan als je je een 2D-vector construct met 3 values? De 3e weggooien? Dat is echt vragen om bugs, kan je beter gewoon een compile-time error van maken (weet niet of je kan static_asserten op een sizeof...() operator, heb alleen MSVC hier geinstalleerd :P)

[ Voor 33% gewijzigd door MLM op 27-12-2012 17:25 ]

-niks-


  • ThomasG
  • Registratie: Juni 2006
  • Laatst online: 23-11 18:54
@MLM, die oplossing was mij even ontschoten, maar lijkt mij beste oplossing. Dan kan ik het _Container template argument laten vallen. Qua cross-product heb je gelijk en zal ik ook voor moeten specializen, maar dat had voor bepaalde types sowieso al gemoeten (integer vectoren hebben bijv. geen epsilon nodig etc.) Specializen vind ik namelijk minder erg dan een class gaan inheriten voor zoiets.

Hoe dieper je in de mogelijkheden van C++ gaan zitten graven, hoe leuker te taal wordt. :)

Edit:
De variadic arguments doe ik op deze manier:
Voordeel hiervan is dat ik dan niet voor elke vector dimensie een nieuwe constructor met een parameter voor elke dimensie moet maken. De eerste twee (x en y) specificeer ik alleen maar omdat het anders conflicten geeft met de default constructor, en de een parameter constructor (dus 1 value voor alle componenten.)

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
template <typename _Component, int _Dimension>
template <typename ... _Variadic>
Vector <_Component, _Dimension>::Vector (
    const _Component componentX,
    const _Component componentY,
    const _Variadic ... components
)
{
    //  _Dimension - 2 omdat sizeof ... (_Variadic) de x en y niet mee rekent.
    static_assert (sizeof ... (_Variadic) == _Dimension - 2, "...");

    //
    this->variadicComponents (componentX, componentY, components);
};

template <typename _Component, int _Dimension>
template <typename ... _Variadic>
void Vector <_Component, _Dimension>::variadicComponents (
    const _Component component,
    const _Variadic ... components
)
{
    const int index = _Dimension - sizeof ... (_Variadic) - 1;

    this->components [index] = component;

    this->variadicComponents (components ...);
};

template <typename _Component, int _Dimension>
void Vector <_Component, _Dimension>::variadicComponents (
    void
)
{
    // doet niks, maar is om de recursie te beëindigen.
    // if (sizeof ... (_Variadic)) {
    //     this->variadicComponents (components ...);
    // }
    // Bovenstaande kan helaas niet, omdat de compiler dan alsog zeurt dat er
    // geen variadicComponents methode is met 0 argumenten, en 
    // const _Component component = 0 oid kan ook niet, omdat het value
    // types zijn.
};


En als je de november 2012 update voor Visual Studio 2012 installeert kun je met MSVC ook C++11 dingen doen (helaas nog niet alles, maar komt er aan in 2013), moet je in je project wel de november compiler als compiler kiezen.

[ Voor 67% gewijzigd door ThomasG op 27-12-2012 17:37 ]


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 01:38

.oisyn

Moderator Devschuur®

Demotivational Speaker

Hou er rekening mee dat anonymous structs zijn geen valide C++11 zijn, al zullen de meeste gangbare compilers ze wel accepteren.

Maar a word of advice: als je een game engine wilt maken, laat dit hele idee varen. Het is teveel hassle en je hebt het nooit nodig. De enige types die je ooit gaat gebruiken zijn 2d, 3d en 4d vectoren van float. Het meest praktisch is dan ook om die specifieke classes te maken, en dan kun je ook meteen gebruik maken van onderliggende native vector registers van het platform (SSE op x86) en bijbehorende intrinsics, wat uiteindelijk veel performantere code oplevert.

Dat neemt natuurlijk niet weg dat het een hele leuke oefening kan zijn, maar houd goed in je achterhoofd wat je uiteindelijke doel is en hoeveel generalisatie je nou daadwerkelijk nodig gaat hebben.

[ Voor 5% gewijzigd door .oisyn op 30-12-2012 02:40 ]

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.


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Met oisyn. Goede tip: laat het idee varen, pak een bestaande library, zoals bijvoorbeeld GLM of de maths classes uit Bullet, en focus op wat je echt wilt doen: een game engine maken, niet een vector library opnieuw uitvinden waar er al een dozijn van zijn die toch beter zijn geschreven na jaren ontwikkeling. (hoewel je dat ook kan zeggen over de game engine zelf... eigenlijk is het beter een bestaande engine uit te breiden met jouw nieuwe ideeen. Maar het is wel leuk natuurlijk; ik ben er zelf ook schuldig aan hehe)

Verder snap ik eigenlijk niet waarom men altijd vector.x wilt schrijven ipv gewoon vector[0]. Zo vaak doe je dat ook eigenlijk niet.

Hier: http://glm.g-truc.net/ Alles wat je wil, alle GLSL types, vectoren, matrices, met swizzles etc en SSE intrinsics. Dat opnieuw schrijven is eigenlijk waanzin. Dan ben je al een jaar bezig met alleen dat :P (hoewel, GLM is al 7 jaar in development )

[ Voor 34% gewijzigd door Zoijar op 30-12-2012 15:46 ]


  • HuHu
  • Registratie: Maart 2005
  • Niet online
vector[0] is helaas niet altijd gelijk aan vector.x. Sommige pakketten doen niet aan xyz, maar aan zyx (eventueel met extra dimensies zoals tijd: tzyx vs. xyzt).

  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

HuHu schreef op zondag 30 december 2012 @ 13:51:
vector\[0] is helaas niet altijd gelijk aan vector.x. Sommige pakketten doen niet aan xyz, maar aan zyx (eventueel met extra dimensies zoals tijd: tzyx vs. xyzt).
Of je interpretatie links- of rechts-handig is doet er niet aanaf dat v[0] of v.x staat voor de eerste component van je vector, wat dat ook moge zijn. Je moet toch gaan converteren (of rekening houden met) tussen een z-up of y-up systeem, je memory ordening, rij of kolom vectoren, links of rechtshandig kruisproduct, quaternion orientatie, en nog een stuk of twintig conventies in 3d/4d vector ruimtes.

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 01:38

.oisyn

Moderator Devschuur®

Demotivational Speaker

Ik ben inderdaad een hoop verschillende conventies tegengekomen (meestal niet gedocumenteerd %&@$@#$ :(), maar dat x niet het eerste component is heb ik nog nooit meegemaakt.

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.


  • HuHu
  • Registratie: Maart 2005
  • Niet online
NumPy doet dat, de volgorde van de dimensies omdraaien. Bijvoorbeeld ndarray.shape, waarbij de argumenten en return-waarden in de vorm zyx zijn, met eventuele extra dimensies daaraan prepended.

Ik gebruik dat samen met een ander pakket dat wel xyz(ctu) doet en waarin lege dimensies een grootte van 1 hebben en niet van 0. Dus als je van dat pakket naar NumPy wilt of andersom, dan moet je steeds de lege dimensies eraf strippen/toevoegen en de volgorde omdraaien.

edit: Matlab heeft er trouwens ook een handje van. 2D matrixen benader je daar via M(row,col). Dus eerst y en daarna x.

[ Voor 10% gewijzigd door HuHu op 30-12-2012 15:53 ]


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 01:38

.oisyn

Moderator Devschuur®

Demotivational Speaker

edit: Matlab heeft er trouwens ook een handje van. 2D matrixen benader je daar via M(row,col). Dus eerst y en daarna x.
Dat is vrij standaard, en heeft weinig met het eerste te maken.

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.


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

HuHu schreef op zondag 30 december 2012 @ 15:50:
NumPy doet dat, de volgorde van de dimensies omdraaien. Bijvoorbeeld ndarray.shape, waarbij de argumenten en return-waarden in de vorm zyx zijn, met eventuele extra dimensies daaraan prepended.

Ik gebruik dat samen met een ander pakket dat wel xyz(ctu) doet en waarin lege dimensies een grootte van 1 hebben en niet van 0. Dus als je van dat pakket naar NumPy wilt of andersom, dan moet je steeds de lege dimensies eraf strippen/toevoegen en de volgorde omdraaien.
Ja, dat is converteren tussen y-up en z-up. Wiskundig is het conventie dat het grondvlak het XZ vlak is, en Y de hoogte, maar in 3D graphics is die conventie dat XY het scherm is en (-)Z de diepte. Kan er ergens nog inkomen dat vector(1,2) daar een 2d vector representeert die wordt opgeslagen als [1,2,0] = [x,z,y] i.e. op het grondvlak. Maar ik zie niet helemaal hoe een selectie op .y het versimpeld, want die moet je dan anders instellen in je library, aangezien de standaard implementatie gewoon zal doen y() {return d[1];}

Je moet dan overigens ook al je matrices aanpassen, je kan niet zomaar je coordinaten omwisselen. Ik zie in mijn code dit, wat vast efficienter kan.

C++:
1
2
3
4
5
6
7
8
9
    if (m_file->getUpAxis().compare("Z_UP", Qt::CaseInsensitive) == 0) {
        const FS::mat4f base_zup(
            1,  0,  0, 0,// column 1 (x-axis)
            0,  0, -1, 0,// column 2 (y-axis)
            0,  1,  0, 0,// column 3 (z-axis)
            0,  0,  0, 1 // column 4 (translation)
        );

        return base_zup * M * inverse(base_zup);
edit: Matlab heeft er trouwens ook een handje van. 2D matrixen benader je daar via M(row,col). Dus eerst y en daarna x.
Dat is ook vrij standaard, wiskundige notatie is namelijk met twee indices in de volgorde rij,kolom, i.e. M12 = element op rij 1, kolom 2. Verder ligt het aan je conventies; als jij al je matrices transposed gebruikt is het wel kolom,rij. In OpenGL is het ook nog eens logisch omdat die matrices column-major opslaat en bij indices de laatste dus de positie binnen een kolom geeft.

Anyway, ik ben m'n punt vergeten :+

[ Voor 13% gewijzigd door Zoijar op 30-12-2012 16:49 ]

Pagina: 1