4
\$\begingroup\$

I started to write a library for linear algebra for personal use, but also for revitilization of my C++.

Below is the first class of this library, a templated vector class, which is templated over the type of the elements as well as the size. It supports all common operations of vectors as member functions and also as static functions, to support different coding preferences. I also overloaded the operators, that make sense in my opinion. I templated over the size because I need the byte-size of a float vector to be 4*n.

Generally all suggestions and comments are more than welcome, but I also have a few points I specifically want to ask about:

  • Interface-Design: Choice of providing both non-static and static versions of most operations, and implementing operators in terms of the static functions
  • Inlining: Did I miss functions, that can be inlined without further thought? Should I remove inlining from some functions
  • Functionality: Did I miss functions/operators that the class should have?
  • Function naming: I'm not completely satisfied with some namen, but can't think of better ones, that are still exact. Maybe someone has suggestions?

Thanks for your time!


template<typename T, uint32_t size>
class Vector {
private:
    std::array<T, size> data;

public:
    Vector() : Vector<T, size>(T(0)) {}

    Vector(T value) {
        for (int i = 0; i < size; ++i) {
            data[i] = value;
        }
    }

    Vector(std::array<T, size> data) {
        this->data = data;
    }

public:
    Vector<T, size>& addTo(const Vector<T, size>& summand) {
        for (int i = 0; i < size; ++i) {
            this->data[i] += summand[i];
        }
        return *this;
    }

    Vector<T, size>& subtractFrom(const Vector<T, size>& subtrahend) {
        for (int i = 0; i < size; ++i) {
            this->data[i] -= subtrahend[i];
        }
        return *this;
    }

    template<typename scalar>
    Vector<T, size>& multiplyBy(scalar factor) {
        for (int i = 0; i < size; ++i) {
            data[i] *= factor;
        }
        return *this;
    }

    Vector<T, size>& multiplyBy(const Vector<T, size>& factor) {
        for (int i = 0; i < size; ++i) {
            data[i] *= factor[i];
        }
        return *this;
    }

    template<typename scalar>
    Vector<T, size>& divideBy(scalar divisor) {
        this->multiplyBy(1 / divisor);
        return *this;
    }

    template<typename otherT>
    Vector<T, size>& divideBy(const Vector<otherT, size>& divisor) {
        for (int i = 0; i < size; ++i) {
            data[i] /= divisor[i];
        }
        return *this;
    }

    template<typename dotResult>
    dotResult dotWith(const Vector<T, size>& other) const {
        dotResult help = dotResult(0);
        for (int i = 0; i < size; ++i) {
            help += data[i] * other[i];
        }
        return help;
    }

    inline T dotWith(const Vector<T, size>& other) const {
        return this->dotWith<T>(other);
    }

    inline T length() const {
        return sqrt(this->lengthSquared());
    }

    T lengthSquared() const {
        T help = 0;
        for (int i = 0; i < size; ++i) {
            help += data[i] * data[i];
        }
        return help;
    }

    Vector<T, size>& normalize() {
        this->divide(this->length());
        return *this;
    }

    T angle(const Vector<T, size>& other) const {
        //Using normalized vectors minimizes rounding problems
        return acos(normalize(*this).dot(normalize(other)));
    }

    Vector<T, size> cross(const Vector<T, size>& other) {
        static_assert(size == 3, "Crossproduct is only defined for Vectors of size 3!");

        Vector<T, size> result;
        result[0] = data[1] * other[2] - data[2] * other[1];
        result[1] = data[2] * other[0] - data[0] * other[2];
        result[2] = data[0] * other[1] - data[1] * other[0];
        return result;
    }

public:
    template<typename scalar>
    static inline Vector<T, size> multiply(Vector<T, size> vector, scalar scalar) {
        return vector.multiplyBy(scalar);
    }

    template<typename scalar>
    static inline Vector<T, size> divide(Vector<T, size> vector, scalar scalar) {
        return vector.divideBy(scalar);
    }

    static inline Vector<T, size> multiply(Vector<T, size> first, const Vector<T, size>& second) {
        return first.multiplyBy(second);
    }

    static inline Vector<T, size> divide(Vector<T, size> first, const Vector<T, size>& second) {
        return first.divideBy(second);
    }

    static inline Vector<T, size> add(Vector<T, size> first, const Vector<T, size>& second) {
        return first.addTo(second);
    }

    static inline Vector<T, size> subtract(Vector<T, size> first, const Vector<T, size>& second) {
        return first.subtractFrom(second);
    }

    template<typename dotResult>
    static inline dotResult dot(Vector<T, size> first, const Vector<T, size>& second) {
        return first.dotWith(second);
    }

    static inline T dot(const Vector<T, size>& first, const Vector<T, size>& second) {
        return dot<T>(first, second);
    }

    static inline Vector<T, size> normalize(Vector<T, size> vector) {
        return vector.normalize();
    }

    static inline Vector<T, size> angle(const Vector<T, size>& first, const Vector<T, size>& second) {
        return first.angle(second);
    }

    static inline Vector<T, size> cross(const Vector<T, size>& first, const Vector<T, size>& second) {
        return first.cross(second);
    }

public:
    inline T& operator[](uint32_t i) {
        assert(i < size);
        return data[i];
    }
    inline T operator[](uint32_t i) const {
        assert(i < size);
        return data[i];
    }

    /*Vector addition*/
    inline Vector<T, size> operator+(const Vector<T, size> summand) const {
        return Vector<T, size>::add(*this, summand);
    }

    /*Vector subtraction*/
    inline Vector<T, size> operator-(const Vector<T, size> subtrahend) const {
        return Vector<T, size>::subtract(*this, subtrahend);
    }

    /*Dot product*/
    inline T operator*(const Vector<T, size> other) const {
        return Vector<T, size>::dot(*this, other);
    }

    /*Scalar multiplication*/
    template <typename scalar>
    inline Vector<T, size> operator*(const scalar& scalar) const {
        return Vector<T, size>::multiply(*this, scalar);
    }
    template <typename scalar>
    inline friend Vector<T, size> operator*(const scalar& scalar, const Vector<T, size>& vector) {
        return Vector<T, size>::multiply(*this, scalar);
    }

    /*Scalar division*/
    template <typename scalar>
    inline Vector<T, size> operator/(const scalar& scalar) const {
        return Vector<T, size>::divideBy(scalar);
    }
    template <typename scalar>
    inline friend Vector<scalar, size> operator/(const scalar& scalar, const Vector<T, size>& vector) {
        return Vector<scalar, size>(scalar).divideBy(vector);
    }

    inline Vector<T, size> operator-() const {
        return Vector<T, size>::multiply(*this, -1);
    }

    inline Vector<T, size> operator+() const {
        return *this;
    }

    inline Vector<T, size>& operator+=(const Vector<T, size>& summand) {
        this->addTo(summand);
    }

    inline Vector<T, size>& operator-=(const Vector<T, size>& subtrahend) {
        this->subtractFrom(subtrahend);
    }

    template<typename scalar>
    inline Vector<T, size>& operator*=(const scalar& factor) {
        this->multiplyBy(factor);
    }

    template<typename scalar>
    inline Vector<T, size>& operator/=(const scalar& divisor) {
        this->divideBy(divisor);
    }

    template <int newSize>
    operator Vector<T, newSize>() const {
        Vector<T, newSize> result;
        for (int i = 0; i < newSize; ++i) {
            result[i] = (i < size) ? data[i] : T(0);
        }
        return result;
    }

    inline bool operator==(const Vector<T, size>& other) {
        for (int i = 0; i < size; ++i) {
            if (data[i] != other[i]) return false;
        }
        return true;
    }

    inline bool operator!=(const Vector<T, size>& other) {
        return !operator==(other);
    }
};

typedef Vector<float, 2> Vector2;
typedef Vector<float, 3> Vector3;
typedef Vector<float, 4> Vector4;
\$\endgroup\$
  • 2
    \$\begingroup\$ LukeG, I'm traveling right now and don't have it handy, Scott Meyer's Effective C++ item 44 covers a big code bloat issue that is related to passing a uint in with your template parameter. Every time that Vector template is instantiated with a different size, it will be copied entirely in the source. This could lead to huge execution binaries. For what should be a trivial amount of code. Google books has a snippet of this item in the preview, but I highly recommend it as a reference book as well. \$\endgroup\$ – James Fegan Jun 7 '16 at 13:54
  • \$\begingroup\$ I should add, that item 44 also covers a refactoring strategy for resolving that issue. \$\endgroup\$ – James Fegan Jun 7 '16 at 13:57
  • \$\begingroup\$ Most of your operations are mutating the *this and then returning it. You should really consider either mutation or returning the result, but not both at the same time. \$\endgroup\$ – Caleth Jun 7 '16 at 14:04
  • \$\begingroup\$ @JamesFegan Just to comment on your point: I actually count on that behaviour, as I need constant size types for use with graphic API (should have mentioned that in the post). \$\endgroup\$ – LukeG Jun 7 '16 at 14:05
  • \$\begingroup\$ @Caleth I'm returning as reference, this allows something like 'v1.addTo(v2).subtractFrom(v3)' which equals 'v1 + v2 - v3'. Do you think it's worthwile to do this or should I cancel this for performance reasons? \$\endgroup\$ – LukeG Jun 7 '16 at 14:07
2
\$\begingroup\$
  1. Take constant arguments by const reference, e.g.

    template<typename T, uint32_t size>
    Vector<T,N>::Vector(std::array<T, size> const&_data) : data(_data) {}
    template<typename T, uint32_t size>
    Vector<T,N>::Vector(T const&_datum) : data(_datum) {}
    

    also, you may want these constructors to be explicit (to disallow implicit usage as converting constructors).

  2. Implement operations via operators only. IMHO, there is not benefit in using the named functions. An implementation via expression templates is rather complicated and only really required in highly performance critical paths (when you may want to do things differently anyway).

    I don't think that the operation scalar/Vector should be supported. This is mathematically/syntactically wrong/dubious.

  3. It makes sense to overload abs(Vector const&vec) to return vec.length().

  4. The cross product is best implemented as standalone function for 3D vectors only, avoiding the need for static_assert. You may also overload the operator^ for this purpose (but consider operator preference).

  5. You should follow the example of std::vector to provide memory access via operator[] w/o checking for out-of-bounds error and via member at() with out-of-bounds error. Don't merely assert, but throw std::out_of_range. (assert should only be used to check internally expected invariants, not user input.)

  6. You may add some functionality to apply an arbitrary function to each element:

    template<typename T, uint32_t size>
    class Vector {
    
      template<typename Func>
      Vector& apply(Func const&func) noexcept(noexcept(func))
      {
        for(uint32_t i=0; i!=size; ++i)
          func(data[i]);
        return*this;
      }
    };
    

    You may also have similar methods for generating another Vector by element-wise operations ...

  7. I would use template alias instead of your typedefs:

    template<typename T> using Vector2 = Vector<T,2>; 
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.