templated classes with optional member attributes


In this article we're going to look at having a templated type, with the existance of a member attribute being dependent on the template arguments.

We're going to pull our motivation from a real world example of creating a vector class, where we want to template out the base vector class to allow it to contain any type, but only have x, y, z, w for vectors that have dimensions that support it.

The fundemamental idea is that since we can not conditionally define member attributes, we use inheritance and combine it with template specialization to make it happen:

  
#include <iostream>

template <typename>
struct OptionalMember {
    T member_name;
};

template <typename>
struct OptionalMember<t,> {};

template <int>
struct MyClass : OptionalMember<int,> {};

int main() {
    MyClass<4> even_instance; // N is even
    even_instance.member_name = 42; // OK, member exists

    MyClass<3> odd_instance;  // N is odd
    // odd_instance.member_name = 10; // ERROR: member_name does not exist

    std::cout << even_instance.member_name << "\n";
}

  

We can extend this idea further to do more complex things, such as passing in more information to the optional member classes if needed. In the following example .y holds a reference to the element at index 1 in the vector.

  
template <typename>= 1)> struct x_ref_base {
    T &x;
    x_ref_base(std::array<t,> &data) : x(data[0]) {}
};

template <typename> struct x_ref_base<t,> {
    x_ref_base(std::array<t,> &) {}
};

template <typename>= 2)> struct y_ref_base {
    T &y;
    y_ref_base(std::array<t,> &data) : y(data[1]) {}
};
template <typename> struct y_ref_base<t,> {
    y_ref_base(std::array<t,> &) {}
};

template <typename>= 3)> struct z_ref_base {
    T &z;
    z_ref_base(std::array<t,> &data) : z(data[2]) {}
};
template <typename> struct z_ref_base<t,> {
    z_ref_base(std::array<t,> &) {}
};

template <typename>= 4)> struct w_ref_base {
    T &w;
    w_ref_base(std::array<t,> &data) : w(data[3]) {}
};
template <typename> struct w_ref_base<t,> {
    w_ref_base(std::array<t,> &) {}
};

template <typename>
struct tvec : public x_ref_base<t,>, y_ref_base<t,>, z_ref_base<t,>, w_ref_base<t,> {
    std::array<t,> data{};

    // NOTE: this constructor is called in every constructor to give the x, y, z, w
    tvec()
        : x_ref_base<t,>(this->data), y_ref_base<t,>(this->data), z_ref_base<t,>(this->data),
          w_ref_base<t,>(this->data) {
        data.fill(T(0));
    }

    tvec(std::initializer_list<t> list) : tvec() {
        assert(list.size() == N);
        std::copy(list.begin(), list.end(), data.begin());
    }

    // allows us to do glm::vec3(0)
    explicit tvec(T val) : tvec() { data.fill(val); }

    // allows us to do glm::vec2(x, y)
    template <typename...> tvec(Ts... vals) : tvec() {
        static_assert(sizeof...(Ts) == N, "Number of arguments must match vector size");
        T tmp[] = {static_cast<t>(vals)...};
        for (size_t i = 0; i < N; ++i) {
            data[i] = tmp[i];
        }
    }
   ...
}
  

Note the importance of every other constructor filtering through the default constructor. This was done to avoid duplication in the other constructors.