Переинтерпретируйте структуру с элементами того же типа, что и массив, стандартным совместимым способом [дубликат]
На этот вопрос уже есть ответ здесь:
В различных кодовых базах 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 ответов:
Нет, это не законно, потому что при добавлении целого числа к указателю применяется следующее ([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] является незаконным.
Как насчет хранения элемента данных в виде массива и доступа к ним по именам?
Для исходного подхода, если x, y и z-все переменные-члены, которые у вас есть, то структура всегда будет иметь размер 3 поплавка, поэтому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]; } };
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 на размер указателя.