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ż. :]