Can you reinterpret an array of doubles as a struct containing doubles?
Asked Answered
R

7

12

Is it OK to cast a double array to a struct made of doubles?

struct A
{
   double x;
   double y;
   double z;
};

int main (int argc , char ** argv)
{
   double arr[3] = {1.0,2.0,3.0};
   A* a = static_cast<A*>(static_cast<void*>(arr));
   std::cout << a->x << " " << a->y << " " << a->z << "\n";
}

This prints 1 2 3. But is it guaranteed to work every time with any compiler?

EDIT: According to

9.2.21: A pointer to a standard-layout struct object, suitably converted ? using a reinterpret_cast, points to its initial member (...) and vice versa.

if I replace my code with

struct A
{
  double & x() { return data[0]; }
  double & y() { return data[1]; }
  double & z() { return data[2]; }
private:
   double data[3];
};

int main (int, char **)
{
   double arr[3] = {1.0,2.0,3.0};
   A* a = reinterpret_cast<A*>(arr);
   std::cout << a->x() << " " << a->y() << " " << a->z() << "\n";
}

then it is guaranteed to work. Correct? I understand that many people would not find this aesteticaly pleasing but there are advantages in working with a struct and not having to copy the input array data. I can define member functions in that struct to compute scalar and vector products, distances etc, that will make my code much easier to understand than if I work with arrays.

How about

int main (int, char **)
{
   double arr[6] = {1.0,2.0,3.0,4.0,5.0,6.0};
   A* a = reinterpret_cast<A*>(arr);
   std::cout << a[0].x() << " " << a[0].y() << " " << a[0].z() << "\n";
   std::cout << a[1].x() << " " << a[1].y() << " " << a[1].z() << "\n";
}

Is this also guaranteed to work or the compiler could put something AFTER the data members so that sizeof(A) > 3*sizeof(double)? And is there any portable way to prevent the compiler from doing so?

Radarscope answered 26/6, 2015 at 21:26 Comment(4)
Anyone can write a compiler, so that's a tough question. Why not specify the compilers you're going to use? My guess is that all major compilers handle this the same and it does work.Manado
Most likely not (in case you want to use not only double there), due to data structure alignmentMcduffie
You break strict aliasing rule.Battement
You could assume that it will work and then add a static_assert test to test that it does work so at least if it doesn’t work you’d know about it.Fulminate
L
11

No, it's not guaranteed.

The only thing prohibiting any compiler from inserting padding between x and y, or between y and z is common sense. There is no rule in any language standard that would disallow it.

Even if there is no padding, even if the representation of A is exactly the same as that of double[3], then it's still not valid. The language doesn't allow you to pretend one type is really another type. You're not even allowed to treat an instance of struct A { int i; }; as if it's a struct B { int i; };.

Limicolous answered 26/6, 2015 at 21:34 Comment(2)
In this case A is standard layout. In theory there can be padding (unlikely with double, though likely with long double) but in any case the struct and the array will have identical padding. This is all assuming there is no #pragma to override the alignment rules. Also A and B are.layout-compatible with each other (though not with the array.)Trisa
@Trisa "but in any case the struct and the array will have identical padding": Nothing in the standard requires that. There can't be padding between the elements of the array, but there can be padding between the class members. That the class is standard-layout only means that there won't be any padding before the first member, nothing more.Hypophosphite
J
5

The standard gives little guarantees about memory layout of objects.

For classes/structs:

9.2./15: Nonstatic data members of a class with the same access control are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified. Implementation alignment requirements might cause two adjacent members not to be allocated immediately after each other; so might requirements for space for managing virtual functions and virtual base classes.

For arrays, the elements are contiguous. Nothing is said about alignment, so it may or may not use same alignment rules than in struct :

8.3.4: An object of array type contains a contiguously allocated non-empty set of N subobjects of type T.

The only thing you can be sure of in your specific example is that a.x corresponds to arr[0], if using a reinterpret_cast:

9.2.21: A pointer to a standard-layout struct object, suitably converted using a reinterpret_cast, points to its initial member (...) and vice versa. [
>

Jala answered 26/6, 2015 at 21:57 Comment(1)
Since this answer has been written, the standard clarified by defect report that the expression a.x has undefined behavior in this situation. It is still true though that standard-layout guarantees that the offset of the x member's address is zero.Hypophosphite
W
1

No it is not guaranteed, even if it should work with all compilers I know on common architectures, because C language specification says :

6.2.6 Representations of types 6.2.6.1 General1 The representations of all types are unspecified except as stated in this subclause. And it says nothing on the default padding in a struct.

Of course, common architectures use at most 64bits which is the size of a double on those architecture, so there should be no padding and your conversion should work.

But beware : you are explicitely invoking Undefined Behaviour, and next generation of compilers could do anything when compiling such a cast.

Wealth answered 26/6, 2015 at 21:41 Comment(0)
B
0

std::complex implementation of msvc use the array solution, and llvm libc++ use the former form.

I think, just check the implementation of the std::complex of your libc++, and use the same solution with it.

Badge answered 5/4, 2018 at 4:44 Comment(1)
std::complex has a special exception in the standard which allows this. It is explicitly not permitted for any other type, even if implemented the same way.Hypophosphite
S
0

Accessing an array as a struct is undefined behavior

Firstly, no, it is undefined behavior to alias an array as a struct. This is simply a strict aliasing violating, as stated in [basic.lval] p11:

If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined:

  • the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
  • a char, unsigned char, or std​::​byte type.

None of these special cases apply, so it is simply UB. You have an object of type "array of double", and it is impossible for an arbitrary struct to alias such a type.

The solution which uses double & x() { return data[0]; } is correct though.

Pointer-interconvertibility does not circumvent strict aliasing

Some people may think that you could circumvent these problems by using something like:

struct A { double arr[3]; };
// ...
double arr[3];
auto p = reinterpret_cast<A*>(&arr); // could p be used?

However, it would not be valid to use p. It is true that an object of type A is pointer-interconvertible with its first non-static data member. However, there exists no object of type A in this scenario.

This rule would only allow you to convert between A and A::arr; it does not circumvent strict aliasing.

What if we ignore strict aliasing for a moment?

While any attempt of aliasing an array as a struct is clearly UB, compilers are rather tolerant when it comes to preserving reinterpret_cast and making it work, even when it is UB.

Even if you made that assumption, you still have the problem that a struct may have different alignment and layout requirements than an array. You could be safe if you added the assertion:

static_assert(sizeof(A) >= sizeof(double[3]));
static_assert(alignof(A) <= alignof(double[3]));

Note that his concern is mostly theoretical; in practice, any sane compiler will lay out three doubles and a double[3] identically.

Are there safer alternatives to reinterpret_cast?

Instead of reinterpret_cast, you can type pun in a number of ways.

double arr[3] = { /* ... */ };

A a = std::bit_cast<A>(arr); // C++20 bit-casting, arguably the best way here

// Implicitly start the lifetime of an A in the same place as arr, keeping the
// value of the doubles.
// Note that arr is not usable as an array afterwards.
A* b = std::start_lifetime_as<A>(arr);

A c;
std::memcpy(&c, arr, sizeof(arr)); // old-school, type-pun through std::memcpy

Note that all methods make the assumption that the layout of A is identical to double[3], and that the alignment of double[3] is at least as strict as that of A.

See also: What is the modern, correct way to do type punning in C++?

What about the other way around, i.e. treat a struct as an array?

It would not be valid to reinterpret a struct containing doubles as a double[3]. If the struct contained a double[3] member, it would also not be valid to reinterpret A* as a double* because arrays aren't pointer-interconvertible with their element type.

However, the following would be valid:

struct A { double arr[3]; };
A a;
double* d = a.arr;
Stipel answered 25/9, 2023 at 0:9 Comment(0)
R
-2

From all I know the answer is: yes.

The only thing that could throw you off is a #pragma directive with some very unusual alignment setting for the struct. If for example a double takes 8 bytes on your machine and the #pragma directive tells to align every member on 16-byte boundaries that could cause problems. Other than that you are fine.

Rivera answered 26/6, 2015 at 21:35 Comment(0)
G
-4

I disagree with the consensus here. A struct with three doubles in it, is exactly the same as an array with 3 doubles in it. Unless you specifically pack the struct differently and are on a weird processor that has an odd number of bytes for doubles.

It's not built into the language, but I would feel safe in doing it. Style wise I wouldn't do it, because it's just confusing.

Gahan answered 26/6, 2015 at 21:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.