Czy Vector3 powinien dziedziczyć po Vector2?

18

Tworzę kilka klas Vector2(X i Y) i Vector3(X, Y i Z), ale nie wiem, czy Vector3odziedziczyć Vector2, czy ponownie zaimplementować zmienne składowe m_xi m_yponownie? Jakie są zalety i wady każdej ze stron (dziedziczenie vs redefinicja).

Edycja: Używam C ++ (VS2010).

Mark Ingram
źródło
1
Dlaczego nie napisać ogólnej klasy wektorów dla n wektorów wymiarowych, a następnie (w razie potrzeby) odziedziczyć klasę vector2 i vector3. Możesz także użyć szablonów dla klasy ogólnej i dziedziczyć wersje dla wektorów całkowitych i wektorów zmiennoprzecinkowych. Edycja: Dlaczego nie korzystasz ze zoptymalizowanej biblioteki matematycznej?
danijar
5
Bez wahania „Vector3 to Vector2”, obaj mogliby odziedziczyć po nadrzędnym VectorN
wim
1
Tak, ale prawdopodobnie będziesz potrzebować wirtualnego stołu i jest to jeden z przypadków, w którym koszty środowiska wykonawczego i pamięci mogą mieć znaczenie. Idealnie, Vector3powinno być tylko 3, floatsjeśli chodzi o pamięć. Nie mówię, że to niemożliwe, tylko że nigdy nie widziałem tego w silniku produkcyjnym.
Laurent Couvidou
2
Tak, tak mi się wydaje. Dopóki nie potrzebujesz niczego innego floats. Wiesz, YAGNI, KISS, wszystkie te rzeczy. Vector2, Vector3I Vector4bez dziedziczenia i floatstylko to naprawdę się de facto standardem w silnikach gier.
Laurent Couvidou
1
Mam nadzieję, że miałeś na myśli typedef float real;;).
Mark Ingram

Odpowiedzi:

47

Nie, nie powinno. Jedyną rzeczą, której będziesz używać po spadku, jestxy elementy i . Metody stosowane w Vector2klasie nie byłyby przydatne w Vector3klasie, prawdopodobnie przyjmowałyby różne argumenty i wykonywały operacje na innej liczbie zmiennych składowych.

MichaelHouse
źródło
+1, powinienem zwrócić większą uwagę na okienko, aby nie pisać zbędnych treści.
Matsemann
8
Nadużycie dziedziczenia klasycznego . Vector3IS-NOT-A Vector2(tak powinno nie dziedziczą), ale AppleIS-A Fruit(więc może dziedziczyć). Jeśli wystarczająco przekręcisz swój umysł, pojawi się w nim Vector3HAS-A Vector2, ale utrata wydajności i trudności w kodowaniu oznaczają, że napiszesz całkowicie oddzielne klasy dla Vector3i Vector2.
bobobobo
Ale możesz (moim zdaniem powinien) napisać n-wymiarową klasę wektorową, aby odziedziczyć wektor 2d i wektor 3d z tego.
danijar
8

Jest ciekawa rzecz, którą możesz zrobić w C ++ (nie określiłeś języka, a ta odpowiedź jest głównie dlatego, że myślę, że fajnie jest widzieć alternatywy, chociaż tak naprawdę nie sądzę, aby było to przydatne w większości przypadków).

Za pomocą szablonów możesz zrobić coś takiego:

template <class T, class S, int U>
class VectorN
{
    protected:
        int _vec[U];
    public:
        S& operator+=(const S c)
        {
            for(int i = 0; i < U; i++)
            {
                _vec[i] += c.at(i);
            }
            return (S&)*this;
        }
        int at(int n) const
        {
            return _vec[n];
        }
};

template <class T>
class Vec2 : public VectorN<T,Vec2<T>,2>
{
    public:
        T& x;
        T& y;
        Vec2(T a, T b) : x(this->_vec[0]), y(this->_vec[1])
        {
            this->_vec[0] = a;
            this->_vec[1] = b;
        }
};

template <class T>
class Vec3 : public VectorN<T,Vec3<T>,3>
{
    public:
        T& x;
        T& y;
        T& z;
        Vec3(T a, T b, T c) : x(this->_vec[0]), y(this->_vec[1]), z(this->_vec[2])
        {
            this->_vec[0] = a;
            this->_vec[1] = b;
            this->_vec[2] = c;
        }
};

i można tego użyć w następujący sposób:

int main(int argc, char* argv[])
{

    Vec2<int> v1(5,0);
    Vec2<int> v2(10,1);

    std::cout<<((v1+=v2)+=v2).x;
    return 0;
}

Jak powiedziałem, nie sądzę, aby było to przydatne i prawdopodobnie skomplikuje twoje życie, gdy spróbujesz wdrożyć kropkę / normalizację / inne rzeczy i spróbujesz być ogólny z dowolną liczbą wektorów.

Łukasz B.
źródło
Tak, cała ogólność wydaje się ładna, tylko przez większość czasu potrzebujesz tylko standardowego 3-wektora z 3 zmiennoprzecinkowymi składnikami - wszystkie nawiasy kątowe sprawią, że będzie Vector3f vbardziej nadętyVector3<float> v
bobobobo
@bobobobo Tak, zgadzam się. Moje klasy wektorowe to zazwyczaj vec2 i vec3 bez elementu nadrzędnego, ale nadal tworzą je szablony. Jeśli przeszkadza Ci pisanie Vector3 <float>, zawsze możesz go wpisać
Luke B.,
..I teraz argument programisty C .. „ale co ze zwiększonym czasem kompilacji przy użyciu szablonów?” Czy naprawdę warto w tym przypadku?
bobobobo
@ bobobobo Nigdy nie miałem żadnych problemów z czasami kompilacji: P, ale nigdy nie pracowałem nad projektem, który kompilowałby czas. Można argumentować, że czasy kompilacji uzasadniają nieużywanie liczb zmiennoprzecinkowych, gdy potrzebne są liczby całkowite.
Łukasz B.
@bobobobo Przy wyraźnych instancjach i bez uwzględnienia pliku wbudowanego w nagłówku czasy kompilacji nie będą się różnić. Dodatkowo wzdęcie wspornika kątowego szablonu jest tylko jeden typedef.
Samaursa,
7

Niezależnie od prędkości, na pierwsze pytanie należy zadać sobie pytanie, kiedy robi każdy dziedziczenie jest jeśli masz zamiar używać ich polimorficznie. Mówiąc dokładniej, czy jest jakakolwiek sytuacja, w której możesz zobaczyć, jak używasz Vector3tak jakby to byłVector2 (który, dziedzicząc po nim, wyraźnie mówisz, że Vector3 „jest-Vector2”).

Jeśli nie, to nie powinieneś używać dziedziczenia. Nie należy używać dziedziczenia do udostępniania kodu. Do tego służą komponenty i funkcje zewnętrzne, a nie to, że i tak dzielisz między nimi dowolny kod.

To powiedziawszy, możesz chcieć łatwych sposobów konwersji Vector3 s na Vector2s, w takim przypadku możesz napisać przeciążenie operatora, które domyślnie skróci je Vector3do a Vector2. Ale nie powinieneś dziedziczyć.

Tetrad
źródło
Dzięki, myślę, że to podkreśliło problem, patrzyłem na to z punktu widzenia „współdzielenia kodu” (tj. Nie musiałem „ponownie wpisywać” wartości X i Y).
Mark Ingram
+1 świetna odpowiedź, nie ma zastosowania polimorficznego między wektorami o różnych rozmiarach.
Łukasz B.
To jest największa rzecz, którą chciałem dodać do własnej odpowiedzi - na pewno +1. (Chociaż istnieje dziwnych okolicznościach, w którym można wyobrazić chcąc polimorfizmu - na przykład 2.5d „” heightmap gier gdzie takie rzeczy kontroli na odległość, itp pathing będzie kanonicznie chcą być wykonane w 2D, ale trzeba jeszcze dostarczyć współrzędne 3D dla obiektów)
Steven Stadnicki
@LukeB. Chociaż w przypadku PO zgadzam się, że wydaje się, że nie ma powodu, aby dziedziczyć po Vector2odziedziczeniu z bazy Vector<N>? To ma sens. Ponadto, dlaczego dziedziczenie automatycznie oznacza zachowanie polimorficzne? Jedną z najlepszych rzeczy w C ++ jest to, że możesz mieć dziedzictwo zerowe koszty. Nie trzeba dodawać żadnych wirtualnych metod (w tym wirtualnych destruktorów) do Vector<N>klasy podstawowej .
Samaursa,
5

Nie, ponieważ każda metoda również będzie musiała zostać przesłonięta, więc nie będziesz miał pożytku z dziedziczenia po niej.

Jeśli coś, oboje mogliby wdrożyć interfejs Vector. Ponieważ jednak prawdopodobnie nie chcesz dodawać / sub / dot / dst między Vector2 i Vector3, będzie to miało niepożądane skutki uboczne. A posiadanie różnych parametrów itp. Byłoby kłopotliwe.
Więc naprawdę nie widzę żadnych zalet dziedziczenia / interfejsu w tym przypadku.

Przykładem jest framework Libgdx, w którym Vector2 i Vector3 nie mają ze sobą nic wspólnego, oprócz tego samego typu metod.

Matsemann
źródło
2

Jeśli planujesz użyć karty SIMD tablic prawdopodobnie najlepiej. Jeśli nadal chcesz używać przeciążania operatora, możesz rozważyć użycie interfejsu / mixinu w celu uzyskania dostępu do podstawowej tablicy - na przykład tutaj jest punkt początkowy, który ma tylko (niesprawdzone) Add.

Zauważ, że nie podałem X/ Y/ Z, każda VectorXklasa dziedziczy bezpośrednio po tej - z tych samych powodów, które określiły inne osoby. Mimo to wiele razy widziałem tablice używane jako wektory na wolności.

#include <xmmintrin.h>

class Vector
{
public:
    Vector(void)
    {
        Values = AllocArray();
    }

    virtual ~Vector(void) 
    { 
        _aligned_free(Values);
    }

    // Gets a pointer to the array that contains the vector.
    float* GetVector()
    {
        return Values;
    }

    // Gets the number of dimensions contained by the vector.
    virtual char GetDimensions() = 0;

    // An example of how the Vector2 Add would look.
    Vector2 operator+ (const Vector2& other)
    {
        return Vector2(Add(other.Values));
    }

protected:
    Vector(float* values)
    {
        // Assume it was created correctly.
        Values = values;
    }

    // The array of values in the vector.
    float* Values;

    // Adds another vector to this one (this + other)
    float* Add(float* other)
    {
        float* r = AllocArray();

#if SSE
        __m128 pv1 = _mm_load_ps(Values);
        __m128 pv2 = _mm_load_ps(other);
        __m128 pvr = _mm_load_ps(r);

        pvr = _mm_add_ps(pv1, pv2);
        _mm_store_ps(r, pvr);

#else
        char dims = GetDimensions();
        for(char i = 0; i < dims; i++)
            r[i] = Values[i] + other[i];
#endif

        return r;
    }

private:

    float* AllocArray()
    {
        // SSE float arrays need to be 16-byte aligned.
        return (float*) _aligned_malloc(GetDimensions() * sizeof(float), 16);
    }
};

Oświadczenie: Mój C ++ może być do kitu, minęło trochę czasu, odkąd go użyłem.

Jonathan Dickinson
źródło
Poczekaj , czy twoje użycie _aligned_mallocoznacza, że błąd, który otworzyłem, nie jest tak naprawdę błędem?
bobobobo
Nie powinieneś używać rzutowań wskaźnika, aby wprowadzić swoje wartości do __m128rejestru, powinieneś użyć _mm_loadu_pszamiast tego. Dobra klasa próbek znajduje się tutaj pod „vectorclass.zip”
bobobobo
@ bobobobo Zrobię najlepszą próbę edycji - zwróć szczególną uwagę na wyłączenie odpowiedzialności;).
Jonathan Dickinson
@ bobobobo _mm_loadu_pspowinien wypracować dla ciebie tę strukturę (gdzie _mm_load_psnie będzie). Dodałem również twoją sugestię - możesz edytować pytanie, jeśli uważasz, że szczekam niewłaściwe drzewo (minęło trochę czasu, odkąd użyłem C [++]).
Jonathan Dickinson
1

Kolejna poważna wada związana z dziedziczeniem Vec3 po Vec2 lub, być może, obojem dziedziczeniem z jednej klasy Vector: kod będzie dużo działałoperacji na wektorach, często w sytuacjach krytycznych czasowo, i w twoim najlepszym interesie leży upewnienie się, że wszystkie te operacje są tak szybkie, jak mogą być - o wiele bardziej niż w przypadku wielu innych obiektów, które nie są dość uniwersalny lub niski poziom. Podczas gdy dobry kompilator dołoży wszelkich starań, aby spłaszczyć wszelkie koszty związane z dziedziczeniem, nadal w większym stopniu polegasz na kompilatorze niż chciałbyś; zamiast tego zbudowałbym je jako struktury z możliwie najmniejszym narzutem, a być może nawet spróbuję uczynić większość funkcji, które ich używają (z wyjątkiem rzeczy takich jak operator +, na które tak naprawdę nie można pomóc), są raczej globalne niż metody na struct. Wczesna optymalizacja jest na ogół zalecana przeciwko, iz nie bez przyczyny,

Steven Stadnicki
źródło
1
-1, ponieważ: klasa i struct mają jedynie implikacje dotyczące przyznania dostępu w C ++, a OP nie określił języka w żaden sposób, nie-wirtualne funkcje składowe mają takie same implikacje wydajnościowe jak funkcje nie będące członkami, a wirtualne funkcje składowe (które potencjalnie wykazują problemy, o które się martwisz) istnieją tylko wtedy, gdy je stworzysz, a nie tylko przez zastosowanie dziedziczenia.
2
@JoshPetrie Ważne punkty na wszystkich frontach; Zwykle „domyślnie” używam C / C ++, więc widziałem to pytanie przez ten obiektyw. Uważam , że istnieją uzasadnione powody (jak również koncepcyjne), aby nie iść drogą dziedziczenia, pamiętajcie, ale mógłbym być znacznie lepszy w konkretnych szczegółach. Spróbuję ponownie to sprawdzić i sprawdzę, czy mogę lepiej księgować.
Steven Stadnicki