Jacques Nel Jacques Nel - 2 months ago 9
C++ Question

How to create variable sized C++ array with named public members i.e. x, y, z, ...?

I am looking for a way to create a general std-like

std::array< T, N >
which has named member elements i.e., x, y, z, ...

Is there a way to use variadic templates to construct such an object, so that the members are conditionally defined via a template parameter?

It is absolutely vital that the object has array-like access via the
operator[]
, and also that its memory footprint is not bloated by extra references or pointers.

I have considered mimicking TR1 traits, but I am really at a loss as to how to begin.

Answer

I am going to answer my own question after playing around for some time. I think I have found what I am looking for although this is just a rough sketch. I will post this, in case others are curious about similar problems.

Definition: Tuple.hpp

#include <cassert>
#include <iostream>

namespace details
{

    template<bool>
    struct rule_is_greater_than_4;

    template<>
    struct rule_is_greater_than_4<true> {};

    template<>
    struct rule_is_greater_than_4<false> {};

    template<class T, size_t N, size_t M>
    class inner_storage : rule_is_greater_than_4< ( M > 4 )>
    {

    public:

        T x, y, z, w;

    private:

        T more_data[ N - 4 ];

    };

    template<class T, size_t N>
    class inner_storage<T, 2, N>
    {

    public:

        T x, y;

    };

    template<class T, size_t N>
    class inner_storage<T, 3, N>
    {

    public:

        T x, y, z;
    };

    template<class T, size_t N>
    class inner_storage<T, 4, N>
    {

    public:

        T x, y, z, w;

    };

}

template<class T, size_t N>
class Tuple : public details::inner_storage<T, N, N>
{

public:

    static_assert( N > 1, "Size of 'n-tuple' must be > 1." );



    // -- Constructors --

    Tuple();

    Tuple( T k );

    Tuple( T x, T y );

    Tuple( T x, T y, T z );

    Tuple( T x, T y, T z, T w );

    // -- Access operators --

    const size_t size();

    T &operator[]( const size_t i );
    T const &operator[]( const size_t i ) const;

    // -- Unary arithmetic operators --

    Tuple<T, N> &operator=( Tuple<T, N> const &t );

    friend std::ostream &operator<<( std::ostream &os, Tuple<T, N> &t );

};

// -- Unary operators --

template<class T, size_t N>
Tuple<T, N> operator+( Tuple<T, N> const &t );

template<class T, size_t N>
Tuple<T, N> operator-( Tuple<T, N> const &t );

// -- Binary arithmetic operators --

template<class T, size_t N>
Tuple<T, N> operator+( Tuple<T, N> const &t1, Tuple<T, N> const &t2 );

template<class T, size_t N>
Tuple<T, N> operator-( Tuple<T, N> const &t1, Tuple<T, N> const &t2 );

template<class T, size_t N>
Tuple<T, N> operator*( T const &s, Tuple<T, N> const &t2 );

template<class T, size_t N>
Tuple<T, N> operator*( Tuple<T, N> const &t1, T const &s );

template<class T, size_t N>
Tuple<T, N> operator/( Tuple<T, N> &t1, T const &s );

// -- Boolean operators --

template<class T, size_t N>
bool operator==( Tuple<T, N> const &t1, Tuple<T, N> const &t2 );

template<class T, size_t N>
bool operator!=( Tuple<T, N> const &t1, Tuple<T, N> const &t2 );

// -- Stream operator --

template<class T, size_t N>
inline std::ostream &operator<<( std::ostream &os, Tuple<T, N> const &t );

#include "Tuple.inl"

Implementation: Tuple.inl

// -- Constructors --

template <class T, size_t N>
Tuple<T, N>::Tuple()
{

}

template <class T, size_t N>
Tuple<T, N>::Tuple( T k )
{

    for( size_t i = 0; i < N; i++ )
    {

        operator[]( i ) = k;

    }

}

template <class T, size_t N>
Tuple<T, N>::Tuple( T x, T y )
{

    static_assert( N == 2, "This constructor is resererved for 2-tuples." );

    this->x = x;
    this->y = y;

}

template <class T, size_t N>
Tuple<T, N>::Tuple( T x, T y, T z )
{

    static_assert( N == 3, "This constructor is resererved for 3-tuples." );

    this->x = x;
    this->y = y;
    this->z = z;

}

template <class T, size_t N>
Tuple<T, N>::Tuple( T x, T y, T z, T w )
{

    static_assert( N == 4, "This constructor is resererved for 4-tuples." );

    this->x = x;
    this->y = y;
    this->z = z;
    this->w = w;

}

// -- Access operators --

template <class T, size_t N>
const size_t Tuple<T, N>::size()
{

    return N;

}

template <class T, size_t N>
T &Tuple<T, N>::operator[]( const size_t i )
{

    assert( i < N );

    return ( &( this->x ) )[ i ];

}

template <class T, size_t N>
T const &Tuple<T, N>::operator[]( const size_t i ) const
{

    assert( i < N );

    return ( &( this->x ) )[ i ];

}

// -- Unary arithmetic operators --

template<class T, size_t N>
Tuple<T, N> &Tuple<T, N>::operator=( Tuple<T, N> const &t )
{

    for( size_t i = 0; i < size(); i++ )
    {

        this->operator[]( i ) = t[ i ];

    }

    return *this;

}

// -- Unary operators --

template<class T, size_t N>
Tuple<T, N> operator+( Tuple<T, N> const &t )
{

    return t;

}

template<class T, size_t N>
Tuple<T, N> operator-( Tuple<T, N> const &t )
{

    return t * T( 0.0f );

}

// -- Binary operators --

template<class T, size_t N>
Tuple<T, N> operator+( Tuple<T, N> const &t1, Tuple<T, N> const &t2 )
{

    Tuple<T, N> sum;

    for( size_t i = 0; i < N; i++ )
    {

        sum[ i ] = t1[ i ] + t2[ i ];

    }

    return sum;

}

template<class T, size_t N>
Tuple<T, N> operator-( Tuple<T, N> const &t1, Tuple<T, N> const &t2 )
{

    Tuple<T, N> difference;

    for( size_t i = 0; i < N; i++ )
    {

        difference[ i ] = t1[ i ] - t2[ i ];

    }

    return difference;

}

template<class T, size_t N>
Tuple<T, N> operator*( Tuple<T, N> const &t, T const &s )
{

    Tuple<T, N> product;

    for( size_t i = 0; i < N; i++ )
    {

        product[ i ] = t[ i ] * s;

    }

    return product;

}

template<class T, size_t N>
Tuple<T, N> operator*( T const &s, Tuple<T, N> const &t )
{

    Tuple<T, N> product;

    for( size_t i = 0; i < N; i++ )
    {

        product[ i ] = s * t[ i ];

    }

    return product;

}

template<class T, size_t N>
Tuple<T, N> operator/( Tuple<T, N> const &t, T const &s )
{

    assert( s != T( 0.0f ) );

    Tuple<T, N> quotient;

    T denom = T( 1.0f ) / s;

    for( size_t i = 0; i < N; i++ )
    {

        quotient[ i ] = t[ i ] * denom;

    }

    return quotient;

}

// -- Boolean operators --

template<class T, size_t N>
bool operator==( Tuple<T, N> const &t1, Tuple<T, N> const &t2 )
{

    bool equal = true;

    for( size_t i = 0; i < N; i++ )
    {

        equal = ( t1[ i ] == t2[ i ] ) ? equal : false;

    }

    return equal;

}

template<class T, size_t N>
bool operator!=( Tuple<T, N> const &t1, Tuple<T, N> const &t2 )
{

    return !( t1 == t2 );

}

// -- Stream operator --

template <class T, size_t N>
inline std::ostream &operator<<( std::ostream &os, Tuple<T, N> const &t )
{

    os << "( ";

    for( size_t i = 0; i < t.size(); i++ )
    {

        os << t[ i ];

        if( i < t.size() - 1 )
        {

            os << ", ";

        }

    }

    os << " )";

    return os;

}

I'm not sure if all the operator overloads are covered, but I wanted to be brief. I'm also not happy with the way the constructors are created (maybe variadic templates could come to the rescue). At least it gives me following desirable functionality:

typedef Tuple<float, 2> Vec2;
typedef Tuple<float, 4> Vec4;
typedef Tuple<float, 10> Vec10;

Vec2 a( 1.0 );

a.x = 3.0f;
a.y = -3.0f;

assert( a[ 1 ] == -3.0f ); // this works

// a.z = 1.0f; ------> compiler error

Vec10 b;

b.x = 0.0f; b.y = 1.0f; b.z = 2.0f; b.w = 3.0f;

b[ 5 ] = 12.0f;

// etc...