Доступ к элементам типа array?


Из-за чтения большого количества предупреждений не использовать союзы, подобные этому:

union rgba
{
    struct
    {
        uint8_t r, g, b, a;
    } components;
    uint8_t index[4];
    uint32_t value;
};

Поскольку это неопределенное поведение, я решил сохранить вещи простыми, как это:

struct rgba
{
    uint8_t r, g, b, a;
};
Но так случилось, что иногда мне действительно нужно получить доступ r, g, b и a в цикле с использованием индексов, иначе мне придется дублировать довольно длинный код для каждого компонента отдельно.

Поэтому я придумал вот что:

struct rgba
{
    u8 r, g, b, a;

    constexpr u8& operator[](size_t x)
    {
        return const_cast<u8*>(&r)[x];
    }
};

Это основано на предположении, что r, g, b и еще a помещаются линейным образом в память, без скрытой шаблонной таблицы между ними, и компилятор сохраняет порядок переменных.

С помощью этого я могу получить доступ к компонентам точно так, как я хотел:

rgba c;
for (int i = 0; i < 3; i++)
    c[i] = i ^ (i + 7);
c.a = 0xFF;
    Поскольку я сделал довольно большие предположения, я уверен, что это еще более неопределенное поведение, чем использование союзов для каламбурирования типов. Я не ошибаюсь?
  1. Как еще я могу достичь аналогичного дизайна?
    1. я хотел бы избежать написания c.r() = 5, Если это возможно, так как это выглядит забавно.
    2. наличие методов доступа, таких как c.components[RED], использует макросы, и мне нравится избегать макросов.
    3. замена макросов из пункта 2. с перечислениями будет выглядеть некрасиво, если принять во внимание пространства имен, необходимые для доступа к такому перечислению. Представьте себе c.components[Image::Channels::Red].
2 2

2 ответа:

Стандарт дает вам ответ на вопрос 1:

9.2/15: нестатические данные члены класса с одинаковым управлением доступом распределяются таким образом, что более поздние члены имеют более высокие адреса внутри объекта класса. Порядок выделения нестатических данных члены с различным контролем доступа не указаны. реализация требования к выравниванию могут привести к тому, что два соседних элемента не будут распределяются сразу же друг за другом; так могли бы требования к пространство для управления виртуальными функциями и виртуальными базовыми классами.

Существует широкий спектр вариантов ответа на вопрос 2. Если вам нравится нотация массива:
  • почему бы не использовать switch(), чтобы безопасно вернуть ссылку на правильный элемент.
  • или лучше, почему бы не заменить ваши члены реальным массивом ?

Первый будет выглядеть так:

struct rgba2
{
    uint8_t r, g, b, a;
    constexpr uint8_t& operator[](size_t x)
    {
        assert(x>=0 && x<4);
        switch (x){
            case 0: return r; 
            case 1: return g;  
            case 2: return b;  
            case 3: return a; 
            //default: throw(1);  // not possible with constexpr
        }
    }
};

И второе:

struct rgba3
{
    enum ergb { red, green, blue, alpha};
    uint8_t c[alpha+1];
    constexpr uint8_t& operator[](ergb x)
    {
        assert(x>=0 && x<4);
        return c[x];
    }
};

Live demo

Вы можете сделать это эффективно в соответствии со стандартами, используя статический массив указателей на члены.

Объем памяти составляет один массив на класс. Генерируемый код идентичен прямому доступу к элементу, когда индекс известен во время компиляции, в оптимизированных сборках.

Вот пример кода (Source):

#include <iostream>

template<class T>
class Vector3
{
public:
   Vector3(const T &xx, const T &yy, const T &zz) :
      x(xx), y(yy), z(zz)
   {
   };

   T& operator[](size_t idx)
   {
      return this->*offsets_[idx];
   };

   const T& operator[](size_t idx) const
   {
      return this->*offsets_[idx];
   };

public:
   T x,y,z;
private:
   static T Vector3::* const offsets_[3];
};

template<class T>
T Vector3<T>::* const Vector3<T>::offsets_[3] =
{
   &Vector3<T>::x,
   &Vector3<T>::y,
   &Vector3<T>::z
};

int main()
{
   Vector3<float> vec(1,2,3);
   vec[0] = 5;
   std::cout << vec.x << std::endl;
}