O utrudnianiu sobie życia (w C++ za pomocą wektorów, metod i szablonów).

16

Taka tylko mała notatka, bo chciałem się pochwalić :>.

To tak tylko ku przestrodze, miało iść do C++ ale tam same pytania i taki 'luźny' temat nie pasował specjalnie. Później jeszcze myślałem nad edukacją żeby zrobić na złość somekindowi, ale ostatecznie idzie do Offtopic, mam nadzieję że chociaż trochę pasuje.

Otóż wyobraźmy sobie że potrzebuję klasy reprezentującej wektor. Pierwsze podejście które robię jest dość naiwne:

struct vector2_t {
    float x;
    float y;
};

(note - wszystko kompilowane z -std=c++0x, trzeba iść z duchem czasu)

Przykład użycia:

vector2_t v;
vector2_t u;

v.x = 3; v.y = 5;
u.x = 8; u.y = 2;

vector2_t w;
w.x = v.x + u.x;
w.y = v.y + u.y;

Nieee... To oczywiście zbyt looserskie rozwiązanie. Porzucam tą myśl od razu. Zacznijmy od tego - co to za klasa która nie ma żadnych metod? Jako dobrzy programista piszę kod obiektowy, prawda? Czy potrzebując dodać dwa wektory mam za każdym razem pisać ręcznie dodawanie? Potrzebuję metod - dopiero wtedy będę mógł spać spokojnie.

struct vector2_t {
    float x;
    float y;

    vector2_t() { }
    vector2_t(float x, float y) : x(x), y(y) { }

    float dot(vector2_t &snd) { return snd.x * x + snd.y * y; }
    float length() { return sqrt(dot(*this)); }

    vector2_t operator+(vector2_t &snd) { return vector2_t(x + snd.x, y + snd.y); }

    vector2_t operator-(vector2_t &snd) { return vector2_t(x - snd.x, y - snd.y); }
};

Przykład użycia:

vector2_t v;
vector2_t u;

v.x = 3; v.y = 5;
u.x = 8; u.y = 2;

vector2_t w = v + u;

Przez chwilę jestem z siebie zadowolony. Jednak po powtórnym spojrzeniu, uświadamiam sobie że kod ten jest nadal w najlepszym wypadku lamerski. Dlaczego przywiązywać się do takiego typu jak float skoro mogę użyć tak wielu innych? To czego potrzebuję to szablony - dopiero wtedy będę mógł spać spokojnie.

template <typename T>
struct vector2_t {
    T x;
    T y;

    vector2_t() { }
    vector2_t(T x, T y) : x(x), y(y) { }

    T dot(vector2_t<T> &snd) { return snd.x * x + snd.y * y; }
    T length() { return sqrt(dot(*this)); }

    vector2_t<T> operator+(vector2_t<T> &snd) { return vector2_t<T>(x + snd.x, y + snd.y); }

    vector2_t<T> operator-(vector2_t<T> &snd) { return vector2_t<T>(x - snd.x, y - snd.y); }
};

Przykład użycia:

vector2_t<float> v;
vector2_t<float> u;

v.x = 3; v.y = 5;
u.x = 8; u.y = 2;

vector2_t<float> w = v + u;

Trzeba właściwie zmienić cały kod korzystający z klasy, ale tym się nie przejmuję. Mam nareszcie rozszerzalną, elastyczną klasę wektora. Czy aby na pewno? Nagle oślepia mnie przerażająca myśl - ten kod wcale nie jest hackerski! Wektor nie jest rozszerzalny! Dlaczego właściwie mam się wiązać z wektorem dwuwymiarowym? Czy gdybym w przyszłości modyfikował projekt żeby działał dla jednego, trzech albo czterech wymiarów mam pisać wszystko od nowa? Kopiować kod? Niweczyć mądrość pokoleń programistów uczących żeby nie powtarzać raz napisanego kodu? Nie! Nigdy!

To czego potrzebuję to wektor który potrafi działać na dowolnej liczbie wymiarów. Dopiero wtedy będę mógł spać spokojnie.

template <typename T>
struct vector_t {
    int dim;
    T *data;

    vector_t(int dim) : dim(dim), data(new T[dim]) { }
    vector_t(T x) { init(1, x, 0, 0, 0); }
    vector_t(T x, T y) { init(2, x, y, 0, 0); }
    vector_t(T x, T y, T z) { init(3, x, y, z, 0); }
    vector_t(T x, T y, T z, T w) { init(4, x, y, z, w); }
    vector_t(int dim, T *data) : dim(dim), data(data) { }

    void init(int dim, T x, T y, T z, T w) {
        this->dim = dim;
        this->data = new T[dim];
                     data[0] = x;
        if (dim > 0) data[1] = y;
        if (dim > 1) data[2] = z;
        if (dim > 2) data[3] = w;
    }

    T &x() { return data[0]; }
    T &y() { if (dim <= 1) { throw "Invalid Op"; } return data[1]; }
    T &z() { if (dim <= 2) { throw "Invalid Op"; } return data[2]; }
    T &w() { if (dim <= 3) { throw "Invalid Op"; } return data[3]; }

    T dot(vector_t<T> &snd) {
        if (dim != snd.dim) { throw "Invalid Arg"; }
        T dot = 0;
        for (int i = 0; i < dim; i++) {
            dot += data[i] * snd.data[i];
        }
        return dot;
    }

    T length() { return sqrt(dot(*this)); }

    vector_t<T> operator+(vector_t<T> &snd) {
        if (dim != snd.dim) { throw "Invalid Arg"; }
        T *d = new T[dim];
        for(int i = 0; i < dim; i++) {
            d[i] = data[i] + snd.data[i];
        }
        return vector_t<T>(dim, d);
    }

    vector_t<T> operator-(vector_t<T> &snd) {
        if (dim != snd.dim) { throw "Invalid Arg"; }
        T *d = new T[dim];
        for(int i = 0; i < dim; i++) {
            d[i] = data[i] - snd.data[i];
        } 
        return vector_t<T>(dim, d);
    }
};

Co można użyć tak:

vector_t<float> v(2);
vector_t<float> u(2);

v.x() = 3; v.y() = 5;
u.x() = 8; u.y() = 2;

Tak, przez chwilę czuję spokój i zadowolenie z dobrze wykonanej roboty i z napisania porządnego, rozszerzalnego kodu. Jednak po chwili przychodzi mi do głowy nowa, straszna myśl - wydajność! Przecież używanie takiego wektora spowoduje straszliwe spowolnienie aplikacji! Nawet jeśli zostanie użyty jedynie kilka razy! Mało tego, jest niebezpieczny! Przecież użytkownik może przypadkowo dodać albo odjąć dwa wektory nie pasujące wielkością...

Zaraz...

To czego potrzebuję to szablony. Więcej szablonów! Wtedy będę mógł spać spokojnie!

template <int D, typename T>
struct vector_t;

template <int D, typename T, int E>
struct vector_dot_t;

template <int D, typename T, int E, typename O>
struct vector_componentwise_t;

template <int D, typename T>
struct vector_coord_t;

template <typename T>
struct add_t {
    T operator() (T a, T b) { return a + b; }
};

template <typename T>
struct sub_t {
    T operator() (T a, T b) { return a - b; }
};

template <int D, typename T>
struct vector_coord_t : vector_coord_t<D - 1, T> {
    T data;
    const T &coord() const { return data; }
    T &coord() { return data; }
};

template <typename T>
struct vector_coord_t<1, T> {
    T x;
    const T &coord() const { return x; }
    T &coord() { return x; }
};

template <typename T>
struct vector_coord_t<2, T> : vector_coord_t<1, T> { 
    T y; 
    const T &coord() const { return y; }
    T &coord() { return y; }
};

template <typename T>
struct vector_coord_t<3, T> : vector_coord_t<2, T> 
{
    T z; 
    const T &coord() const { return z; }
    T &coord() { return z; }
};

template <typename T>
struct vector_coord_t<4, T> : vector_coord_t<3, T> {
    T w; 
    const T &coord() const { return w; } 
    T &coord() { return w; } 
};

template <int D, typename T>
struct vector_t : public vector_coord_t<D, T> { 
    T dot(vector_t<D, T> &other) const
    { return vector_dot_t<D, T, D>::dot(*this, other); }

    T length() const
    { return sqrt(vector_dot_t<D, T, D>::dot(*this, *this)); }

    vector_t<D, T> operator+ (vector_t<D, T> other) const {
        vector_t<D, T> out;
        vector_componentwise_t<D, T, D, add_t<T>>::componentwise(*this, other, out);
        return out;
    }

    vector_t<D, T> operator+= (vector_t<D, T> other) {
        *this = *this + other;
    } 

    vector_t<D, T> operator- (vector_t<D, T> other) const {
        vector_t<D, T> out;
        vector_componentwise_t<D, T, D, sub_t<T>>::componentwise(*this, other, out);
        return out;
    }

    vector_t<D, T> operator-= (vector_t<D, T> other) {
        *this = *this - other;
    } 
};

template <int D, typename T, int E>
struct vector_dot_t {
    static T dot(const vector_t<D, T> &fst, const vector_t<D, T> &snd) {
        return static_cast<vector_coord_t<E, T>>(fst).coord() *
            static_cast<vector_coord_t<E, T>>(snd).coord() +
            vector_dot_t<D, T, E - 1>::dot(fst, snd);
    }
};

template <int D, typename T>
struct vector_dot_t<D, T, 0> {
    static T dot(const vector_t<D, T> &fst, const vector_t<D, T> &snd) 
    { return 0; }
};

template <int D, typename T, int E, typename O>
struct vector_componentwise_t {
    static void componentwise(const vector_t<D, T> &fst,
            const vector_t<D, T> &snd,
            vector_t<D, T> &out) {
        O fx;
        static_cast<vector_coord_t<E, T>&>(out).coord() = fx(
                static_cast<vector_coord_t<E, T>>(fst).coord(),
                static_cast<vector_coord_t<E, T>>(snd).coord());
        vector_componentwise_t<D, T, E - 1, O>::componentwise(fst, snd, out);
    }
};

template <int D, typename T, typename O>
struct vector_componentwise_t<D, T, 0, O> {
    static void componentwise(const vector_t<D, T> &fst,
            const vector_t<D, T> &snd,
            vector_t<D, T> &out) { }
};

oraz:

vector_t<2, float> v;
vector_t<2, float> u;

v.x = 3; v.y = 5;
u.x = 8; u.y = 2;

vector_t<2, float> w = v + u;

I tak oto dochodzimy do końca tej smutnej historii :>. W zasadzie przedostatnia wersja została napisana tylko na potrzeby tego posta a ostatnią pisałem bardziej dla ćwiczenia, wiedząc że to nieszczególnie dobra droga, ale tak mniej więcej szło moje rozumowanie - poprawić ten kod, żeby był lepszy.

Po napisaniu ostatniej wersji zastanowiłem się i zrobiłem rollback do pierwszej, czystej 'POD' struktury - morał z tego krótki i wszystkim znany: prosty kod to dobry kod, dziękuję za uwagę.

Otóż tylko tyle, chwalenia koniec, i mojej urzekającej historii również. :]

0

Weź tu człeku taki kod debuguj potem... ;)

0

Sytuacja zazwyczaj wygląda tak, że albo piszesz prosty i wydajny kod specjalizowany do konkretnego zastosowania, albo ogólny, przewidujący wiele przypadków za to skomplikowany i powolny.

3

też tak często "polepszam" dobrze działający kod

myślę że tak naprawdę to forma prokrastynacji i oszukiwanie samego siebie że się coś robi :D

btw to trochę dziwne że tematy o programowaniu na forum programistycznym lądują w dziale off-topic ;)
tak jakby programiści już totalnie nie mieli o czym innym porozmawiać ;D
przydałby się osobny dział, a raczej zmiana nazwy i/lub opisu któregoś z obecnych - typuję dział "Edukacja" albo "Inne" ;)

0

Zapomniałeś o konstruktorach przesuwających (czy jak tam się to zwie).
No i wykorzystać SSE4 + asm...

1

Przede wszystkim, jeśli ma być obiektowo, to Vector powinien dziedziczyć z Matrix. ;)

0
somekind napisał(a):

Przede wszystkim, jeśli ma być obiektowo, to Vector powinien dziedziczyć z Matrix. ;)

jeżeli to miałoby być obiektowo to wektor 1d, 2d, 3d i 4d powinny być osobnymi klasami skoro nie są ze sobą kompatybilne (nie da się ich dodawać ani odejmować między sobą)
a zamiast tego jest tutaj jakieś ukrywanie widocznych publicznie własności rzucając wyjątkami i sprawdzanie czy są jednego typu zamiast zrzucenia tego na kompilator :|

co najwyżej 4d mógłby dziedziczyć z 3d, 3d z 2d itd.
dzięki temu możliwe by było obcięcie któregoś z wymiarów i dodanie do siebie wektorów po zrzutowaniu do wspólnego typu
ale to też bez sensu

według podejścia z tego kodu równie dobrze można by było zrobić jedną klasę która zastąpi wszystkie inne klasy, miałaby w cholerę własności i w zależności od podanego w konstruktorze typu klasy dawała dostęp do odpowiedniej części z nich... :|

1
unikalna_nazwa napisał(a):

jeżeli to miałoby być obiektowo to wektor 1d, 2d, 3d i 4d powinny być osobnymi klasami skoro nie są ze sobą kompatybilne (nie da się ich dodawać ani odejmować między sobą)
a zamiast tego jest tutaj jakieś ukrywanie widocznych publicznie własności rzucając wyjątkami i sprawdzanie czy są jednego typu zamiast zrzucenia tego na kompilator :|

Nie, to by nie było programowanie obiektowe, to by było programowanie planetarne.

Wektor wektorów? Istnieje coś takiego? Wektor można przedstawić jako macierz 1 x n albo n x 1, dlatego jest szczególnym przypadkiem macierzy.

0

moze przeladowany operator []? w przypadku klasy szablonowej specjalizowanej , rozszerzonej z matrix<0,n> ?

0

Niedługo opublikuję podobny kod jako Open Source - case prawie identyczny (czyli jak skomplikować mapę<string,unia>).
Ale kodu jest tam tyle że trochę powątpiewam czy ktoś będzie chciał to rozwijać...
Mało wydajne, ale bardzo ułatwia pracę.

0

Pisze się to, co jest potrzebne. Jak będzie potrzebne więcej, to się dopisze.
Trzeba rozróżniać sytuację, gdy piszesz bibliotekę ogólnego zastosowania na jakiś temat, od sytuacji gdy masz rozwiązać konkretny problem.
W pierwszym — i tylko tym pierwszym — przypadku można sobie pozwolić na pisanie klas „kompletnych”. W drugim: ma działać. Nic więcej.

5
Azarien napisał(a):

Pisze się to, co jest potrzebne. Jak będzie potrzebne więcej, to się dopisze.
Trzeba rozróżniać sytuację, gdy piszesz bibliotekę ogólnego zastosowania na jakiś temat, od sytuacji gdy masz rozwiązać konkretny problem.
W pierwszym — i tylko tym pierwszym — przypadku można sobie pozwolić na pisanie klas „kompletnych”. W drugim: ma działać. Nic więcej.

no niby - ale mi się często włącza tryb myślenia:

"ale fajna ta klasa, na pewno będę chciał ją jeszcze gdzieś wykorzystać, więc uzupełnijmy ją teraz o te 750 warunków i 40 zbędnych obecnie metod żebym potem mógł ją tylko z przyjemnością używać w każdej sytuacji" ;)

po czym zapominam że kiedykolwiek tę klasę napisałem lub po roku kiedy patrzę na nią okazuje się tak ch*jowo napisana że wolę ją napisać od początku ;D

tak się nie dzieje tylko gdy czas goni ;)

0

No cóż Bracia - zasada KISS się kłania ;)

3

Nie KISS, a YAGNI.

Pisze się to, co jest potrzebne. Jak będzie potrzebne więcej, to się dopisze.
Trzeba rozróżniać sytuację, gdy piszesz bibliotekę ogólnego zastosowania na jakiś temat, od sytuacji gdy masz rozwiązać konkretny problem.
W pierwszym — i tylko tym pierwszym — przypadku można sobie pozwolić na pisanie klas „kompletnych”. W drugim: ma działać. Nic więcej.

W bibliotekach ogólnego zastosowania też nie wrzuca się funkcji bez przemyślenia tego. Na przykład twórcy Apache Wicket nie są chętni, żeby wrzucać do tego frameworka webowego tysiąca kontrolek, choć byłoby to łatwe, bo późniejsza lekka zmiana architektury wymagałaby wielu zmian. Poza tym duuużo łatwiej jest coś dodać do publicznego frameworka niż usunąć, bo przecież jak już coś jest to ludzie mają tendencję do korzystania z tego, nawet jak nie jest to coś najwyższych lotów.

0

Pod koniec jeszcze trzeba zdac sobi sprawe, ze ludzkie rozszerzalne i szablonowe rozwiazanie siedzi boost.geometry jako model::point

template<typename CoordinateType, std::size_t DimensionCount, typename CoordinateSystem>
class model::point
{
  // ...
};

1 użytkowników online, w tym zalogowanych: 0, gości: 1