6

I'm attempting to create a color class of variable size- given a template-determined array of values, I'd like to create named aliases of each value in the array, ie:

template<int C = 3, typename T = unsigned char>
class Color {
public:
  union {
    T v[C];
    struct {
      T r, g, b, a;
    };
  };
};

However, if I try to use the same class for C=3, the union mandates a size of 4 bytes (the 'a' member). Alternatively, using a mathematically expressed bitfield size for a (struct named a, anonymous T member, size evaluates to 1 at C>3), the compiler issues a permissive warning (non-suppressible, as per In gcc, how to mute the -fpermissive warning? ), something unsuitable for a larger-scale API.

How would I go about allowing a single class to handle different numbers of variables, while retaining per-variable names and without implementing recursive-include macro magic (tried this, shouldn't have). Thanks in advance!

Edit: To clarify the question, an answer to any of the following will solve this problem:

  • Suppress GCC's -fpermissive errors (#pragma diagnostic ignored doesn't work for permissive)
  • Set maximum size of union or child struct not to exceed C bytes
  • Allow bitfield length of 0 for members not covered by C bytes (GCC allows mathematical expressions for bitfield length, such as (C-3 > 0)?8:0; )
  • Disable members not covered by C bytes by some other means (ie, mythical static_if() )
Community
  • 1
  • 1
Precursor
  • 622
  • 7
  • 18
  • So if I understand correctly you want the number of members of the anonymous struct to vary with the template parameter `C`? – Mark B Mar 09 '15 at 19:42
  • Correct- ideally, something like a static_if could switch on additional a,x,y,z,w members based on C >= 4... – Precursor Mar 09 '15 at 19:43
  • Do you need that `T v[C];` thing or did you just include it because you expected that to be needed for implementation? – jplatte Mar 09 '15 at 19:44
  • It's preferable to include, as it allows for much cleaner code while writing the implementation, but it's not the end of the world without- &r[x] gives essentially the same functionality. – Precursor Mar 09 '15 at 19:46
  • 2
    This is probaby UB. If I'm not wrong, "r" is not required to map to v[0] exactly(neither to v[3]). – Not a real meerkat Mar 09 '15 at 19:47
  • To my understanding (and experience), within a union and disallowing automatic padding/alignment, each member is stored at the same location, where the union is the max of all member lengths- thus, with 'r' first, &r == [the address of 'v'] That said, I'll edit my question to be more clear on the question I'm asking – Precursor Mar 09 '15 at 19:50
  • How about a partial specialization for different cases of C? – Vaughn Cato Mar 09 '15 at 19:58
  • I just noticed that you use an anonymous struct there. That's non-portable, so I will completely remove the union from the answer that I'm about to write. I have an idea on how to still have the actual data as an array, but I'll have to play around with it to see whether or not it actually works. – jplatte Mar 09 '15 at 20:00
  • 1
    @CássioRenan It's complicated. I'm not sure what you mean by "map", but `r` is required to be stored at the same address as `v[0]`, as both the initial element of an array and the initial member of a standard-layout class are guaranteed to be stored without any prior padding, as Precursor commented as well. But after setting `r`, reading `v[0]` is still not allowed, nor vice versa, as far as I'm aware. The standard doesn't define the behaviour of that, and optimisers may optimise based on the assumption that the program does not attempt to do so. –  Mar 09 '15 at 20:01
  • @Precursor BTW, beware that anonymous structs are a non-standard feature. Standard C++ only has anonymous unions. –  Mar 09 '15 at 20:04
  • @hvd "after setting r, reading v[0] is still not allowed, nor vice versa (...) The standard doesn't define the behaviour of that" -> This is mostly what I tried to say. Still, nice to know they are required to be in the same address. – Not a real meerkat Mar 09 '15 at 20:16

2 Answers2

4

You could make a specialization of the struct for different cases of C:

template <int C = 3, typename T = unsigned char> union Color;

template <typename T>
union Color<3,T> {
  T v[3];
  struct {
    T r,g,b;
  };
};

template <typename T>
union Color<4,T> {
  T v[4];
  struct {
    T r,g,b,a;
  };
};

Note that anonymous structs are non-standard.

If using member functions is a possibility, I think that would be a better way to go:

template <int C,typename T>
class Color {
  public:
    using Values = T[C];

    Values &v() { return v_; }

    const Values &v() const { return v_; }

    T& r() { return v_[0]; }
    T& g() { return v_[1]; }
    T& b() { return v_[2]; }

    template <int C2 = C,
      typename = typename std::enable_if<(C2>3)>::type>
    T& a()
    {
      return v_[3];
    }

    const T& r() const { return v_[0]; }
    const T& g() const { return v_[1]; }
    const T& b() const { return v_[2]; }

    template <int C2 = C,
      typename = typename std::enable_if<(C2>3)>::type>
    const T& a() const
    {
      return v_[3];
    }

  private:
    Values v_;
};

You can then use it like this:

int main()
{
  Color<3,int> c3;
  Color<4,int> c4;

  c3.v()[0] = 1;
  c3.v()[1] = 2;
  c3.v()[2] = 3;

  std::cout <<
    c3.r() << "," <<
    c3.g() <<"," <<
    c3.b() << "\n";

  c4.v()[0] = 1;
  c4.v()[1] = 2;
  c4.v()[2] = 3;
  c4.v()[3] = 4;

  std::cout <<
    c4.r() << "," <<
    c4.g() << "," <<
    c4.b() << "," <<
    c4.a() << "\n";
}
Vaughn Cato
  • 63,448
  • 5
  • 82
  • 132
  • Really appreciate the detail provided in your answer- I was hesitant to use member functions, but specialized structs (partial templating) was just what I needed. Many thanks! – Precursor Mar 10 '15 at 01:40
2

Okay, so now @VaughnCato had this one out before me, but I'll still post my answer using std::enable_if. It declares Color as struct, because there is really no point in having a class when everything is public (and there is no good reason to declare the data member [v in the question] private), adds template aliases for a bit more syntactic sugar and uses static_assert to make sure the user doesn't use weird values for the template parameters. It also uses std::enable_if in a slightly different manner, which I would argue is a bit more readable.

#include <type_traits>
#include <cstdint>

template<unsigned nChans, typename T = std::uint8_t>
struct Color
{
    static_assert(nChans >= 3 || nChans <= 4,   "number of color channels can only be 3 or 4");
    // allow integral types only
    //static_assert(std::is_integral<T>::value,   "T has to be an integral type");
    // also allow floating-point types
    static_assert(std::is_arithmetic<T>::value, "T has to be an arithmetic (integral or floating-point) type");

    T data[nChans];

    T& r() { return data[0]; }
    T& g() { return data[1]; }
    T& b() { return data[2]; }

    //template<typename U = T, typename EnableIfT = std::enable_if<(nChans == 4), U>::type> // C++11
    template<typename U = T, typename EnableIfT = std::enable_if_t<(nChans == 4), U>> // C++14
    T& a() { return data[3]; }

    const T& r() const { return data[0]; }
    const T& g() const { return data[1]; }
    const T& b() const { return data[2]; }

    //template<typename U = T, typename EnableIfT = std::enable_if<(nChans == 4), U>::type>
    template<typename U = T, typename EnableIfT = std::enable_if_t<(nChans == 4), U>>
    T const& a() const { return data[3]; }
};

template<typename T = std::uint8_t> using RgbColor  = Color<3, T>;
template<typename T = std::uint8_t> using RgbaColor = Color<4, T>;
jplatte
  • 1,121
  • 11
  • 21