A template vector wrapper class for C++.
Find a file
2015-07-25 21:58:56 +02:00
include/vectorwrapper Allow casting to vectors of lower dimensions. 2015-07-25 21:18:45 +02:00
test Allow casting to vectors of lower dimensions. 2015-07-25 21:18:45 +02:00
.travis.yml Fix in travis.yml. 2015-07-24 22:35:54 +02:00
LICENSE Initial commit 2015-07-23 01:33:36 +02:00
README.md Remove Build status section. 2015-07-25 21:58:56 +02:00

Vector Wrapper

Image Build status

Motivation

Short version

A vector wrapper is mainly useful if used as an adapter between vectors from different libraries. It basically removes the need to explicitly construct a vector of type B when you have a vector of type A. It's roughly equivalent to using a reinterpret_cast on your vectors, but the code tries really hard to make sure the cast is valid in all cases, wrapping it nicely behind a clean interface. Implicit constructors also allow to convert easily from one type to the other.

Long version

We all need vectors in our code, and there are quite a few implementations out there. They all do their job, and in terms of interface they are pretty similar to each other, but not exactly the same thing. While this is not an issue in its own, it can become a problem when your code depends on external libraries that will provide their own vector implementation. So you might have your own implementation, MyVector, and the library will give you Ogre::Vector3, which is what you must use when you invoke Ogre's functions. If you also depend on openCV you will have yet another vector around. Microsoft Kinect SDK also imposes you its own, barebone vector implementation.

While through discipline you can manage such cases more or less well, there is one major issue that I couldn't work around: your own library code. I like to write code I think I will re-use at some point in the future in libraries separate from the main program. Odds are at some point you will need some maths that are not covered by the library vector you are using. Unless it's something really particular, I will probably try to write it in some library so that other projects can benefit from my new formula. The good thing about low level libraries is that they have very few dependencies, or none at all, and can thus be carried around and adapted to a variety of projects. It's all nice and good, but how would you add your new function taking a Ogre::Vector3 into a library that depends on nothing without bringing in the whole Ogre3D library?

This is where my class comes into play.

Implementation

The basic idea is that all vectors will somehow give you access to some x, y, z coordinates. You will do that very frequently in your code, maybe on different platforms and in time critical parts of your code, so virtual calls (or equivalent) are not an option. This library is entirely templated, and will let you wrap any type inside a Vec. In fact, you could even just use this wrapper as your vector of choice, using an std::array<float, 3> as the wrapped type and forget how it's working under the hood.

Unfortunately the fact that you use Vec<std::array<float, 3>> and Vec<Ogre::Vector3> also means that you still have several types of vectors in your program. The good news is that conversion is automated (no more Ogre::Vector3(other_vec.X, other_vec.Y, other_vec.Z) in your code) and often it will not even be necessary, as casting will suffice.

You still need to type some code in order to get started using the vector wrapper class, which is basically your custom implementation of a struct listing informations about the wrapped type. That's not a lot of code.

Features

Automated conversion

You can assign or construct a Vec<A> from a Vec<B>, provided they have the same dimensions and the assignemnt operator is able to convert B's scalar type to that of A.

Access the wrapped type

Through the data() method you can always obtain a ref to the wrapped type.

Dimension change

Depending on the wrapped type, Vec can provide methods to get a Vec of lower or higher dimensions. For example, Vec<Ogre::Vector3> can optionally expose a xy() method that is equivalent to Vec<Ogre::Vector2>(vec3.x(), vec3.y()).

Casting

A must have some fundamental properties before it can be casted to B:

  • casting won't result in a misaligned object
  • the casted object won't go past the memory reserved to the original object (in other words sizeof(B) <= sizeof(A) - offset of the first coordinate in A)
  • B must be a standard layout struct
  • B has no data before the first coordinate
  • Coordinates in B appear in the same order and at the same offsets as they appear in A
  • B is exactly as large as dimensions * sizeof(scalar_type), but you can disable this constraint by setting cast_ignore_trailing_properties=1 in B's VectorWrapperInfo

For example you could conceptually wrap a type A:

struct A {
    int something;
    float x, y, z;
    std::string something_else;
};

and still have it castable to type B:

struct B {
    float x, y, z;
};

Vectors must still have the same dimensions. cast<type>() is only available when you define offset_x (see Usage). Either way, to cast (reinterpret_cast actually, be careful about the strict aliasing rule) you can just do the following:

typedef Vec<A> avec;
typedef Vec<B> bvec;

avec a(0);

some_function(bvec(a)); //creates a temporary
some_function(a.cast<bvec>()); //reinterpret_cast

Usage

In this example we will adapt std::array<float, N> and Ogre::VectorN. In your real code, write the VectorWrapperInfo specializations for your own wrapped types in some header file, and just include it in your code to use Vec with those types.

//Include Vec's header
#include "vectorwrapper.hpp"
#include <array>
#include <cstddef>

template <>
struct VectorWrapperInfo<std::array<float, 3>> {
    enum {
        //Tell Vec this is a 3D vector
        dimensions = 3
    };
    
    //The inner type is a float
    typedef float scalar_type;
    
    //Not required by Vec but useful for us
    typedef std::array<float, 3> vector_type;
    
    //This will make the xy(), xz() and yz() methods available
    typedef std::array<float, 2> lower_vector_type;
    
    static scalar_type& get_at (size_t parIndex, vector_type& parVector) {
        //parIndex is already asserted to be < dimensions
        //so you can always assume it's valid.
        return parVector[parIndex];
    }
};

template <>
struct VectorWrapperInfo<std::array<float, 2>> {
    enum {
        dimensions = 2
    };
    
    typedef float scalar_type;
    typedef std::array<float, 2> vector_type;
    typedef std::array<float, 3> higher_vector_type;
    typedef std::array<float, 1> lower_vector_type;
    
    static scalar_type& get_at (size_t parIndex, vector_type& parVector) {
        return parVector[parIndex];
    }
};

//Not really useful I suppose, but this is just an example
template <>
struct VectorWrapperInfo<float> {
    enum {
        dimensions = 1
    };
    
    typedef float scalar_type;
    typedef float vector_type;
    typedef std::array<float, 2> higher_vector_type;
    
    static scalar_type& get_at (size_t, vector_type& parVector) {
        return parVector;
    }
};

template <>
struct VectorWrapperInfo<Ogre::Vector3> {
    enum {
        dimensions = 3
    };
    
    typedef Ogre::Real scalar_type;
    typedef Ogre::Vector3 vector_type;
    typedef Ogre::Vector2 lower_vector_type;
    
    //If you want, you can implement the get_at() method as we did
    //for std::array, returning *(&parVector.x + parIndex)
    //but you can let Vec do that for you and just provide some
    //offsets. By doing this, you also give a hint to Vec about
    //what the wrapped type's layout is, so it will enable
    //certain casts that would be disabled if you provided
    //the get_at() method instead.
    enum {
        offset_x = offsetof(Ogre::Vector3, x),
        offset_y = offsetof(Ogre::Vector3, y),
        offset_z = offsetof(Ogre::Vector3, z)
    };
};

template <>
struct VectorWrapperInfo<Ogre::Vector2> {
    enum {
        dimensions = 2
    };
    
    typedef Ogre::Real scalar_type;
    typedef Ogre::Vector2 vector_type;
    typedef Ogre::Vector3 higher_vector_type;
    
    enum {
        offset_x = offsetof(Ogre::Vector3, x),
        offset_y = offsetof(Ogre::Vector3, y)
    };
};

typedef Vec<std::array<float, 3>> avec3;
typedef Vec<std::array<float, 2>> avec2;
typedef Vec<float> avec1;
typedef Vec<Ogre::Vector3> ovec3;
typedef Vec<Ogre::Vector2> ovec2;

Limitations

  • 4D vectors are not currently supported
  • Read-only vectors are not supported (not sure if such a thing is really needed)