Plebshot
Plebshot

Reputation: 220

Create member alias based on a template parameter condition C++17

So, I am trying to simplify the use of my generic classes and came across the following idea:

The following struct is given:

template <size_t size>
struct Vector {
    std::array<float, size> data;

    float& x = data[0];
    float& y = data[1]; // Declare y only if size is > 0
    float& z = data[2]; // Declare z only if size is > 1
    float& w = data[3]; // Declare w only if size is > 2
};

Obviously, if I try to run the program like this, the array will throw an out of range exception.

Now is there a way to declare these aliases only if the condition (known at compile time) is given?

I thought of something in the manner of std::enable_if:

template <size_t size>
struct Vector {
    std::array<float, size> data;

    float& x = data[0];
    declare_if<(size > 0), float&> y = data[1];
    declare_if<(size > 1), float&> z = data[2];
    declare_if<(size > 2), float&> w = data[3];
};

Also, I would prefer to not let the class derive from another, or fully specialize the class.

Upvotes: 2

Views: 272

Answers (6)

YSC
YSC

Reputation: 40100

If having functions returning reference instead of reference public member is an option, you can go with:

#include <array>
#include <type_traits>

template<size_t size>
struct Vector
{
    std::array<float, size> data;

    template<size_t s = size, std::enable_if_t<s >= 1, int> = 0> float& x() { return data[0]; }
    template<size_t s = size, std::enable_if_t<s >= 2, int> = 0> float& y() { return data[1]; }
    template<size_t s = size, std::enable_if_t<s >= 3, int> = 0> float& z() { return data[2]; }
    template<size_t s = size, std::enable_if_t<s >= 4, int> = 0> float& w() { return data[3]; }
};

Upvotes: 1

virgesmith
virgesmith

Reputation: 822

I agree with the other answers suggesting use functions rather than references to access. A reason not mentioned so far is that with the references you cannot enforce constness on the data: a float& in a const object is still modifiable even if its referencing a const member variable. Example derived from your code:

#include <iostream>

template <size_t size>
struct Vector {

  Vector() { std::fill(data, data + 4, 0.0f); }

  float data[4];

  float& x = data[0];
  float& y = data[1]; 
  float& z = data[2]; 
  float& w = data[3]; 

  // read-write access
  float& getx() { return data[0]; }
};

int main()
{
  const Vector<4> v;

  //++v.data[0]; // compile error

  std::cout << v.x << std::endl;
  ++v.x; // modifies const through non-const ref!
  std::cout << v.x << std::endl;

  //++v.getx(); // compile error (member function enforces const)

}

Upvotes: 0

SirGuy
SirGuy

Reputation: 10780

I recommend approaching this a little bit differently than creating members that are references to other members (you have to manually implement the assignment operators for each one if you want your class to be assignable).

Why not some free functions that do the accessing for you?

template <std::size_t i, std::size_t size>
auto & get(Vector<size> & v) { return std::get<i>(v.data); }

template <std::size_t size>
auto & x(Vector<size> & v) { return get<0>(v.data); }

template <std::size_t size>
auto & y(Vector<size> & v) { return get<1>(v.data); }

//...

This will give a compile-time error if you try to access a member in a vector that has insufficient size

Upvotes: 1

bolov
bolov

Reputation: 75825

AFAIK you can do that and keep your syntax only with inheritance or specialization.

If you want to avoid that you need to change a bit the interface. You need to make x, y, z, t methods. Actually method templates:

template <size_t size>
struct Vector {
    std::array<int, size> data;

    template <std::size_t S = size, class = std::enable_if_t<(S > 0)>>
    auto x() -> int& { return data[0]; };

    template <std::size_t S = size, class = std::enable_if_t<(S > 1)>>
    auto y() -> int& { return data[1]; };

    template <std::size_t S = size, class = std::enable_if_t<(S > 2)>>
    auto z() -> int& { return data[2]; };

    template <std::size_t S = size, class = std::enable_if_t<(S > 3)>>
    auto t() -> int& { return data[3]; };  
};
Vector<2> v;

v.x();
v.y();
v.z(); // error: no matching member function for call to 'z'

Upvotes: 5

bipll
bipll

Reputation: 11940

The most probable is to resort to specialization:

template <size_t size>
class Vector {
    std::array<float, size> data;

    float& x = data[0];
    float& y = data[1]; // Declare y only if size is > 1
    float& z = data[2]; // Declare z only if size is > 2
    float& w = data[3]; // Declare w only if size is > 3
};
template<> class Vector<0> {
    std::array<float, 0> data;
};
template<> class Vector<1> {
    std::array<float, 1> data;
    float &x = data[0];
};

(Note that I've changed size limits in the comments to not exceed array's bounds.)

Additionally, if you don't like the idea of full specialization, well, you'll need to introduce those members in the generic case anyway, but their default bindings can change:

template <size_t size>
class Vector {
    std::array<float, size> data;

    float& x = data[0];
    float& y = data[std::min(size - 1, 1)];
    float& z = data[std::min(size - 1, 2)];
    float& w = data[std::min(size - 1, 3)];
};

(If you're ok with x, y, z and w referring to the same array element.)

Upvotes: 2

zen-cat
zen-cat

Reputation: 425

Why not declare a function template float& get() which you can std::enable_if based on size?

Upvotes: -2

Related Questions