Search code examples
c++templatesmember-variablesnon-type-template-parameter

Declare a number of member variables of different types in templated class according to template parameter


Is it possible to define a class template in such a way that the number of member variables for each templated class depends on a template parameter (i.e. a std::size_t)? I'll refer to this template parameter in question as the "dimension".

Currently, I have defined a set of ordinary classes that correspond to dimension == 2 and dimension == 3.

For dimension == 2, these set of classes amount to the following:

class Scalar2 {}; // a "grade-0" object
class Vector2 {}; // a "grade-1" object
class Bivector2 {}; // a "grade-2" object

class Multivector2
{
public:
    Multivector2(Scalar2 scal, Vector2 vec, Bivector2 bivec);
private:
    Scalar2 scal;
    Vector2 vec;
    Bivector2 bivec;
};

For dimension == 3, we have

class Scalar3 {}; // a "grade-0" object
class Vector3 {}; // a "grade-1" object
class Bivector3 {}; // a "grade-2" object
class Trivector3 {}; // a "grade-3" object

class Multivector3
{
public:
    Multivector3(Scalar3 scal, Vector3 vec, Bivector3 bivec, Trivector3 trivec);
private:
    Scalar3 scal;
    Vector3 vec;
    Bivector3 bivec;
    Trivector3 trivec;
};

So, for dimension n, there are n+1 graded classes (from grade 0 to grade n), and then a single multivector class that contains one of each of the individual grades.

Now I want to extend this to an arbitrary number of dimensions. So, in effect, I want something along the lines of this:

template<std::size_t n /* dimension */, std::size_t k /* grade */>
class K_Vector {};

template<std::size_t n /* dimension */>
class Multivector
{
public:
    Multivector(K_Vector<n,0> kvec0, K_Vector<n,1> kvec1, K_Vector<n,2> kvec2, ... K_Vector<n,n> kvecn); 
private:
    K_Vector<n,0> kvec0;
    K_Vector<n,1> kvec1;
    K_Vector<n,2> kvec2;
    ...
    K_Vector<n,n> kvecn;
};

Of course, this doesn't compile. The key issue is that the number of member variables depends on the value of the dimension parameter, n. A similar issue can also be seen regarding how the arguments of the constructor are defined.

As far as I'm aware, there isn't a way to declare multiple member variables programmatic such as in a loop. This wouldn't be particularly ideal anyways as naming of individual members would be fairly problematic. So, storing the members inside a container like std::vector sounds like a step in the right direction. Problem is, std::vector requires its elements to be the same. In my case, each member variable in Multivector has a different type.

One option I've considered is to derive the graded classes from a base class, and store the different components inside a vector of pointers to the base:

class K_Vector_Base {};

template<std::size_t n /* dimension */, std::size_t k /* grade */>
class K_Vector : public K_Vector_Base {};

template<std::size_t n /* dimension */>
class Multivector
{
public:
    Multivector(std::vector<std::unique_ptr<K_Vector_Base>> kvecs); 
private:
    std::vector<std::unique_ptr<K_Vector_Base>> kvecs;
};

However, this is not particularly ideal: to avoid object-slicing, and since the Multivector needs to own its graded subcomponents, we need to store them as unique_ptrs, which feels like a waste for storing grade classes that are individually lightweight anyways. It would no longer be possible for a Multivector to be stored contiguously. Furthermore, this approach would need to involve a lot of casting from the base to the individual types; the individual graded classes are too dissimilar in how their data are represented, and how they are involved in arithmetic operations, that the advantages of virtual methods cannot be employed. e.g. double dot_product(Vector, Vector), Vector dot_product(Vector, Bivector), and double dot_product(Bivector, Bivector) would use different logic, and return different types; these need to be separate free functions, not virtual functions.

Are there any approaches that would result with sets of classes that more closely resembles the dimension == 2 and dimension == 3 specific cases? (Note: classes that correspond to a particular dimension number are expected to be able to coexist and interact via arithmetic operations. Classes of different dimension would not be compatible with each other; you can add a K_Vector<3,1> to a K_Vector<3,2> (both dimension 3), but you cannot add a K_Vector<3,1> to a K_Vector<2,1>).


Solution

  • You could use a std::tuple to store the different K_Vectors:

    #include <cstddef>
    #include <tuple>
    #include <utility>
    
    template <std::size_t N /* dimension */>
    class Multivector {
    public:
        // the storage type:
        using kvecs_t = decltype([]<std::size_t... Is>(std::index_sequence<Is...>) {
            return std::tuple<K_Vector<N, Is>...>{};
        }(std::make_index_sequence<N + 1>{}));
    
        template <class... Args> // constructor
        Multivector(Args&&... args) : m_kvecs{std::forward<Args>(args)...} {}
    
        // getters:
        template <size_t I>
        auto& get() { return std::get<I>(m_kvecs); }
    
        template <size_t I>
        const auto& get() const { return std::get<I>(m_kvecs); }
    
        template <class T>
        auto& get() { return std::get<T>(m_kvecs); }
        
        template <class T>
        const auto& get() const { return std::get<T>(m_kvecs); }
    
    private:
        kvecs_t m_kvecs;
    };
    

    Example usage:

    int main() {
        Multivector<2> mv(K_Vector<2, 0>{}, K_Vector<2, 1>{}, K_Vector<2, 2>{});
        
        auto& kv0 = mv.get<K_Vector<2, 0>>();  // get reference to K_Vector<2, 0>
        auto& kv2 = mv.get<2>();  // get reference to K_Vector<2, 2>
    }
    

    Demo


    As noted by Jarod42, the unconstrained constructor will match a non-const copy constructor. In order to constrain it to match on N + 1 K_Vectors you could add an extra layer where the implementation takes a pack of size_ts instead of just the one N:

    template <class Seq> class MultivectorImpl;
    
    template <std::size_t... Is>
    class MultivectorImpl<std::index_sequence<Is...>> {
    public:
        static inline constexpr size_t N = sizeof...(Is) - 1;
    
        MultivectorImpl(K_Vector<N, Is>... args) : m_kvecs{std::move(args)...} {}
    
        // getters, same as above
    
    private:
        std::tuple<K_Vector<N, Is>...> m_kvecs;
    };
    
    template <std::size_t N>
    using Multivector = MultivectorImpl<std::make_index_sequence<N + 1>>;
    

    Demo