In various 3d math codebases I sometimes encounter something like this:
struct vec {
float x, y, z;
float& operator[](std::size_t i)
{
assert(i < 3);
return (&x)[i];
}
};
Which, AFAIK is illegal because implementations are allowed to spuriously add padding between members, even if they are of the same type, though none will do so in practice.
Can this be made legal by imposing constraints via static_assert
s?
static_assert(sizeof(vec) == sizeof(float) * 3);
I.e. does static_assert
not being triggered implies operator[]
does what is expected and doesn't invoke UB at runtime?
You can never be sure that this will work
There is no guarantee of contiguity of subsequent members, even if this will frequently work perfectly in practice thanks to usual float alignment properties and permissive pointer arithmetic.
This is laid down in the following clause of the C++ standard:
There is no way to make this legal using
static_assert
noralignas
constraints. All you can do is to prevent the compilation, when the elements are not contiguous, using the property that the address of each object is unique:But you can reimplement the operator to make it standard compliant
A safe alternative, would be to reimplement
operator[]
to get rid of the contiguity requirement for the three members:Note that an optimizing compiler will generate very similar code for both the reimplementation and for your original version (see an example here). So rather choose the compliant version.
How about storing the data member as array and access them by names?
EDIT: For the original approach, if x, y and z are all the member variables you have, then the struct will always be the size of 3 floats, so
static_assert
can be used for checking thatoperator[]
will access within bounded size.See also: C++ struct member memory allocation
EDIT 2: Like Brian said in another answer,
(&x)[i]
itself is undefined behaviors in the standard. However, given that the 3 floats are the only data members, the code in this context should be safe.To be pedantic on syntax correctness:
Although this will increase each vec by the size of a pointer.
Type aliasing (use of more then one type for essentially the same data) is a huge problem in C++. If you keep member functions out of structs and maintain them as PODs, things ought to work. But
can't make accessing one type as another technically legal. In practice of course there will be no padding, but C++ isn't clever enough to realise that vec is an array of floats and an array of vecs is an array of floats constrained to be a multiple of three, and the casting &vecasarray[0] to a vec * is legal but casting &vecasarray[1] is illegal.
According to the standard, it is clearly Undefined Behaviour, because you either do pointer arithmetics outside of an array or alias the content of a struct and an array.
The problem is that math3D code can be used intensively, and low level optimization makes sense. The C++ conformant way would be to directly store the array, and use accessors or references to individual members of the array. And neither of those 2 options are perfectly fine:
accessors:
The problem is that using a function as a lvalue is not natural for old C programmers:
v.x() = 1.0;
is indeed correct but I'd rather avoid a library that would force me to write that. Of course we could use setters, but if possible, I prefere to writev.x = 1.0;
thanv.setx(1.0);
, because of the common idiomv.x = v.z = 1.0; v.y = 2.0;
. It is only my opinion, but I find it neater thanv.x() = v.z() = 1.0; v.y() = 2.0;
orv.setx(v.sety(1.0))); v.setz(2.0);
.references
Nice! We can write
v.x
andv[0]
, both representing the same memory... unfortunately, the compilers are still not smart enough to see that the refs are just aliases for an in struct array and the size of the struct is twice the size of the array!For those reasons, the incorrect aliasing is still commonly used...
No, it is not legal because when adding an integer to a pointer, the following applies ([expr.add]/5):
y
occupies the memory location one past the end ofx
(considered as an array with one element) so adding 1 to&x
is defined, but adding 2 to&x
is undefined.