std::memcpy struct with tightly packed members of TriviallyCopyable type T to an array of T and vise versa

1.4k Views Asked by At

Here is has been established that it is illegal to treat tightly packed consecutive struct members of type T as an array of T.

But what about copying the underlying representation?

Given:

struct vec {
    float x, y, z;
};

with the the same constraints:

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

is the following:

int main() {
    vec v = {1.9f, 2.5f, 3.1f};

    float a[3];
    std::memcpy(&a, &v, 3 * sizeof(float));
    assert(a[0] == v.x);
    assert(a[1] == v.y);
    assert(a[2] == v.z);

    vec u;
    std::memcpy(&u, &a, 3 * sizeof(float));
    assert(u.x == a[0]);
    assert(u.y == a[1]);
    assert(u.z == a[2]);
}

legal?

2

There are 2 best solutions below

0
On BEST ANSWER

As long as your structure type doesn't have any padding, there's no explicit support for it in the standard, but support for something very close to it can be inferred.

Given a trivially copyable type T, what's explicitly allowed is to copy its representation into an array of char (or unsigned char) and back.

There is no requirement that the contents of the array are preserved in the array itself. The contents could be stored in a file, and re-read on a subsequent execution of the program. Or stored in an object of a different type, so long as that type allows it. For this to work, implementations must allow memcpying representations into objects when those representations did not originate from objects of type T in that same run.

As a result, at the very least,

int main() {
    vec v = {1.9f, 2.5f, 3.1f};

    float a[3];

    assert(sizeof v == sizeof a);

    { char tmp[3 * sizeof(float)];
      std::memcpy(tmp, &v, 3 * sizeof(float));
      std::memcpy(a, tmp, 3 * sizeof(float)); }
    assert(a[0] == v.x);
    assert(a[1] == v.y);
    assert(a[2] == v.z);

    vec u;
    { char tmp[3 * sizeof(float)];
      std::memcpy(tmp, a, 3 * sizeof(float));
      std::memcpy(&u, tmp, 3 * sizeof(float)); }
    assert(u.x == a[0]);
    assert(u.y == a[1]);
    assert(u.z == a[2]);
}

should either fail on the first assert, or pass. For any representation where it'd fail, it's trivial to create a function which happens to come up with that exact representation in unambiguously valid ways, therefore it mustn't fail.

Now, omitting tmp here is a bit iffy.

std::memcpy is just repeated assignments of the individual bytes and could have been spelled out explicitly. The semantics of the = operator imply that for trivially copyable types, a = b; and { auto tmp = b; a = tmp; } are equivalent. Same with a = b; c = d; and { auto tmp1 = b; auto tmp2 = d; a = tmp1; c = tmp2; }, and so on. The former is what a direct memcpy does, the latter is what two memcpys through tmp do.

On the other hand, the permission to copy in and out of an array of char could be read as requiring an actual array of char, not merely the functional equivalent of it.

Personally, I probably would not worry about that unless I actually came across an implementation which uses that interpretation, but if you want to play it safe, you can introduce such a temporary array, and verify that your compiler manages to optimise it away.

7
On

The concern why you shouldn't always trust that a struct of three members of the same type is equivalent to an array of the same type is basically because of memory alignment.

https://en.wikipedia.org/wiki/Data_structure_alignment

Your code could run alright on a C++ compiler and fail on another or even on the same compiler with different configuration.

Also notice that you are using the array pointer incorrectly.

std::memcpy(a, &v, 3 * sizeof(float));

and not

std::memcpy(&a, &v, 3 * sizeof(float));

a is already a constant pointer to float