Переинтерпретируйте структуру с элементами того же типа, что и массив, стандартным совместимым способом [дубликат]


На этот вопрос уже есть ответ здесь:

В различных кодовых базах 3d math я иногда сталкиваюсь с чем-то вроде этого:

struct vec {
    float x, y, z;

    float& operator[](std::size_t i)
    {
        assert(i < 3);
        return (&x)[i];
    }
};

Который, AFAIK является незаконным, потому что реализации позволено спонтанно добавлять заполнение между членами, даже если они одного типа, хотя никто не сделает этого на практике.

Можно ли сделать это законным, наложив ограничения через static_assert s?
static_assert(sizeof(vec) == sizeof(float) * 3);

Т. е. означает ли static_assert, что не срабатывает operator[], что это ожидается и не вызывает UB во время выполнения?

5 7

5 ответов:

Нет, это не законно, потому что при добавлении целого числа к указателю применяется следующее ([expr.добавить] / 5):

Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива, или на один прошедший последний элемент массива объекта, оценка не должна производить переполнение; в противном случае поведение является не определено.

y занимает место в памяти на единицу дальше конца x (рассматривается как массив с одним элементом) , поэтому добавление 1 к &x является определено, но добавление 2 к &x не определено.

Вы никогда не можете быть уверены, что это сработает

Нет никакой гарантии смежности последующих членов, даже если это часто будет работать идеально на практике благодаря обычным свойствам выравнивания с плавающей точкой и разрешающей арифметике указателей. Нет никакого способа сделать это законным, используя static_assert или alignas ограничения. Все, что вы можете сделать, это предотвратить компиляцию, когда элементы не являются смежными, используя свойство, что адрес каждого объекта уникален:
    static_assert (&y==&x+1 && &z==&y+1, "PADDING in vector"); 

Но вы можете переопределить оператор, чтобы сделать его стандартным

Безопасной альтернативой было бы переосмысление operator[], Чтобы избавиться от требования смежности для трех членов:

struct vec {
    float x,y,z; 

    float& operator[](size_t i)
    {
        assert(i<3); 
        if (i==0)     // optimizing compiler will make this as efficient as your original code
            return x; 
        else if (i==1) 
            return y; 
        else return z;
    }
};
Обратите внимание, что оптимизирующий компилятор будет генерировать очень похожий код как для реимплементации, так и для вашей исходной версии (см. пример здесь). Так что скорее выбирайте совместимую версию.

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

Проблема заключается в том, что math3d-код может использоваться интенсивно, и оптимизация низкого уровня имеет смысл. С++ - конформный способ заключается в непосредственном хранении массива и использовании методов доступа или ссылок на отдельные элементы массива. И ни один из этих двух вариантов не является идеальным отлично:

  • Методы доступа:

    struct vec {
    private:
        float arr[3];
    public:
        float& operator[](std::size_t i)
        {
            assert(i < 3);
            return arr[i];
        }
        float& x() { return arr[0];}
        float& y() { return arr[0];}
        float& z() { return arr[0];}
    };
    

    Проблема в том, что использование функции в качестве lvalue не является естественным для старых программистов C: v.x() = 1.0; действительно правильно, но я предпочел бы избегать библиотеки, которая заставила бы меня написать это. Конечно, мы могли бы использовать сеттеры, но если это возможно, я предпочитаю писать v.x = 1.0;, чем v.setx(1.0);, из-за общей идиомы v.x = v.z = 1.0; v.y = 2.0;. Это только мое мнение, но я нахожу его более точным, чем v.x() = v.z() = 1.0; v.y() = 2.0; или v.setx(v.sety(1.0))); v.setz(2.0);.

  • Ссылки

    struct vec {
    private:
        float arr[3];
    public:
        float& operator[](std::size_t i)
        {
            assert(i < 3);
            return arr[i];
        }
        float& x;
        float& y;
        float& z;
        vec(): x(arr[0]), y(arr[1]), z(arr[2]) {}
    };
    

    Здорово! Мы можем запишите v.x и v[0], представляющие одну и ту же память... к сожалению, компиляторы все еще недостаточно умны, чтобы понять, что ссылки-это просто псевдонимы для массива in struct, а размер структуры в два раза больше размера массива!

По этим причинам неправильное сглаживание все еще широко используется...

Алиасирование типов (использование более одного типа для практически одних и тех же данных) является огромной проблемой в C++. Если вы держите функции-члены вне структур и поддерживаете их как модули, все должно работать. Но

  static_assert(sizeof(vec) == sizeof(float) * 3);

Не может сделать доступ к одному типу как к другому технически законным. На практике, конечно, не будет никакого заполнения, но C++ недостаточно умен, чтобы понять, что vec-это массив поплавков, а массив Vec-это массив поплавков, ограниченный кратностью трем, и приведение &vecasarray[0] для vec * является законным, но кастинг &vecasarray[1] является незаконным.

Как насчет хранения элемента данных в виде массива и доступа к ним по именам?

struct vec {
    float p[3];

    float& x() { return p[0]; }
    float& y() { return p[1]; }
    float& z() { return p[2]; }

    float& operator[](std::size_t i)
    {
        assert(i < 3);
        return p[i];
    }
};
Для исходного подхода, если x, y и z-все переменные-члены, которые у вас есть, то структура всегда будет иметь размер 3 поплавка, поэтому static_assert можно использовать для проверки того, что operator[] будет иметь доступ в пределах ограниченного размера.

Смотрите также: C++ struct memory allocation

EDIT 2: Как сказал Брайан в другом ответе, (&x)[i] само по себе является неопределенным поведением в стандарте. Однако, учитывая, что 3 поплавка являются единственными членами данных, код в этом контексте должен быть безопасным.

Быть педантичным по синтаксической правильности:

struct vec {
  float x, y, z;
  float* const p = &x;

  float& operator[](std::size_t i) {
    assert(i < 3);
    return p[i];
  }
};

Хотя это увеличит каждый vec на размер указателя.