-1

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.

Jacques Nel
  • 581
  • 2
  • 11
  • 3
    What do you actually want to achieve, and why? – πάντα ῥεῖ Oct 03 '16 at 20:29
  • 1
    Tuple does not have operator[] because members can be of different type. Drop the operator[] and just use structs. – Alexey Guseynov Oct 03 '16 at 20:30
  • I want all elements to be of the same type. Let me be more specific. Say I wish to have a object which can behave as an n-dimensional vector where the first 4 elements are named x, y, z, and w. A quaternion would have w denote the first element. – Jacques Nel Oct 03 '16 at 20:33
  • 2
    What goes after "z" in this ellipsis? :) – Mikhail Oct 03 '16 at 20:33
  • 1
    @JacquesNel then you simply do not want tuple. Tuples are for different types. – Mikhail Oct 03 '16 at 20:34
  • Imagine `N` is a template parameter. Then essentially I want x, y, z, .. up to `N` elements. If `N > 4`, I want to have an array of the same type extending for an additional `N - 4` elements. – Jacques Nel Oct 03 '16 at 20:35
  • 1
    @JacquesNel `std::array` fits all your needs except for x, y, z, w members. – Mikhail Oct 03 '16 at 20:36
  • As @Mikhail has pointed out, I am confusing the `std::tuple` with the formal Mathematical definition. Thanks. For the sake of the question how would you then create something similar to `std::array` with named members? – Jacques Nel Oct 03 '16 at 20:38
  • Well, for this I'd recommend 1) update text of the question (absolutely must), 2) declare template class and provide five partial specializations for N == 1, 2, 3, 4, >4 (this one is arguable). – Mikhail Oct 03 '16 at 20:39
  • @Mikhail What is the best way to implement the N > 4 case? – Jacques Nel Oct 03 '16 at 20:46

3 Answers3

1

There is nothing nice to achieve it:

#include <cstddef>

template <std::size_t Extent>
struct Tuple;

// You cant not use a template for declaring member names.
// Hence a set of pre-declared tuples.

template <> struct Tuple<1> {
    double x;
    double operator [] (std::size_t idx) const { return x; }
};

template <> struct Tuple<2> {
    double x;
    double y;
    double operator [] (std::size_t idx) const { return (idx < 1) ? x : y; }
};

template <> struct Tuple<3> {
    double x;
    double y;
    double z;
    double operator [] (std::size_t idx) const {
        return (idx < 1)
            ? x
            : (idx < 2)
                ? y
                : z;
    }
};

// However, you pay the price of conditional evaluation for accessing
// the members via operator [].

// An alternative is accessing the values through a std::array and member functions:

template <> struct Tuple<1> {
    std::array<double, 1> data;
    double x() const { return data[0]; }
    double operator [] (std::size_t) const { return data[0]; }
};

template <> struct Tuple<2> {
    std::array<double, 2> data;
    double x() const { return data[0]; }
    double y() const { return data[1]; }
    double operator [] (std::size_t idx) const { return data[idx]; }
};

template <> struct Tuple<3> {
    std::array<double, 3> data;
    double x() const { return data[0]; }
    double y() const { return data[1]; }
    double z() const { return data[2]; }
    double operator [] (std::size_t idx) const { return data[idx]; }
};

// However, now you have to write tuple.x(), which is annoying
// in mathematical equations.

For completeness: Another option is type punning through a union. But that is a move to the dark side of undefined behavior (See: Opinions on type-punning in C++?).

Community
  • 1
  • 1
  • I think it is better to also add non-const versions of `x()`, `y()`, `z()` and `operator[]` that return reference to data. – Mikhail Oct 04 '16 at 06:15
1

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...
Jacques Nel
  • 581
  • 2
  • 11
  • If you have a working solution, it is better to post it rather than "just a rough sketch". It is also a good practice to attach link to a demo on e.g. ideone.com. Otherwise your answer is quite good. However, if you don't mind to use `.x()` instead of `x`, the second answer of Dieter Lücking is simpler (you just need to add non-const methods that return references instead of values). – Mikhail Oct 04 '16 at 06:14
  • `rule_is_greater_than_4` and both `N` and `M` look redundant, but one needs a working code to tell for sure. – Mikhail Oct 04 '16 at 06:18
  • Thank you @Mikhail for your suggestions. I'm still learning the ins and outs of this site, so I really appreciate any suggestion on common posting practice. I will update the code to a full implementation, and I will check out the site that you mentioned. – Jacques Nel Oct 04 '16 at 06:18
  • Also concerning "Tuple not allowed, doesn't make sense": 1. If you use it instead of `float` - yes. If you use it in generic code working with Tuples of unknown lengths - why not? 2. It might not have sense in your particular domain, but might have perfect sense in some other domain. As a general rule, if you write some generic entity, it is better not to make it biased to a particular domain. – Mikhail Oct 04 '16 at 06:22
0

Say you want to store types of int

using mytype = int;
struct mytuple_ {
  mytype x;
  mytype y;
  mytype z;
} tup;

This meets your criteria of not adding any extra memory usage and simply lays out the data in memory.

To say set x=5

tup.x = 5;
Chuck Norrris
  • 284
  • 5
  • 12