2

Can std::memmove() be used to "move" the memory to the same location to be able to alias it using different types?

For example:

#include <cstring>
#include <cstdint>
#include <iomanip>
#include <iostream>

struct Parts { std::uint16_t v[2u]; };

static_assert(sizeof(Parts) == sizeof(std::uint32_t), "");
static_assert(alignof(Parts) <= alignof(std::uint32_t), "");

int main() {
    std::uint32_t u = 0xdeadbeef;

    Parts * p = reinterpret_cast<Parts *>(std::memmove(&u, &u, sizeof(u)));

    std::cout << std::hex << u << " ~> "
              << p->v[0] << ", " << p->v[1] << std::endl;
}
$ g++-10.2.0 -Wall -Wextra test.cpp -o test -O2 -ggdb -fsanitize=address,undefined -std=c++20 && ./test
deadbeef ~> beef, dead

Is this a safe approach? What are the caveats? Can static_cast be used instead of reinterpret_cast here?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
jotik
  • 17,044
  • 13
  • 58
  • 123
  • 1
    You still don't have a proper `Parts` object. The portable approach to create a trivial object via memory representation is to have a `Parts p;` and then `memcpy` to `&p`. `memmove` is irrelevant here. – François Andrieux Apr 08 '21 at 13:56
  • I'm pretty sure this is undefined behavior, with or without `memmove`. The code accesses `Parts` object whose lifetime has never started. I don't see how `memmove` changes that. – Igor Tandetnik Apr 08 '21 at 13:56
  • @IgorTandetnik But isn't `struct Parts` an [implicit-lifetime type](https://en.cppreference.com/w/cpp/language/lifetime#Implicit-lifetime_types) which is [created](https://en.cppreference.com/w/cpp/language/object#Object_creation) by [memmove](https://en.cppreference.com/w/cpp/string/byte/memmove)? – jotik Apr 08 '21 at 13:59
  • 1
    There is no `struct Parts` object in the code example. There's a `std::uint32_t`. There's a `struct Parts*`, which points to an object that is not a `struct Parts`. – Eljay Apr 08 '21 at 14:07
  • FYI C++20 introduced [`std::bit_cast`](https://en.cppreference.com/w/cpp/numeric/bit_cast) as a safe, convenient way to do this. The cppreference page has an example implementation that you can use if your compiler's not providing it yet (due in GCC 11 FWIW). – Tony Delroy Apr 08 '21 at 15:04
  • @TonyDelroy: I wouldn't trust clang or gcc to process such an implementation of `bit_cast` correctly. Neither compiler will reliably handle constructs which change the type of storage, but write either a bit pattern that matches what storage already holds, or a bit pattern that will be overwritten before it is observed. – supercat Apr 08 '21 at 20:11

1 Answers1

0

If one is interested in knowing what constructs will be reliably processed by the clang and gcc optimizers in the absence of the -fno-strict-aliasing, rather than assuming that everything defined by the Standard will be processed meaningfully, both clang and gcc will sometimes ignore changes to active/effective types made by operations or sequences of operations that would not affect the bit pattern in a region of storage.

As an example:

#include <limits.h>
#include <string.h>

#if LONG_MAX == LLONG_MAX
typedef long long longish;
#elif LONG_MAX == INT_MAX
typedef int longish;
#endif

__attribute((noinline))
long test(long *p, int index, int index2, int index3)
{
    if (sizeof (long) != sizeof (longish))
        return -1;

    p[index] = 1;
    ((longish*)p)[index2] = 2;

    longish temp2 = ((longish*)p)[index3];
    p[index3] = 5; // This should modify p[index3] and set its active/effective type
    p[index3] = temp2; // Shouldn't (but seems to) reset effective type to longish

    long temp3;
    memmove(&temp3, p+index3, sizeof (long));
    memmove(p+index3, &temp3, sizeof (long));
    return p[index];
}
#include <stdio.h>
int main(void)
{
    long arr[1] = {0};
    long temp = test(arr, 0, 0, 0);
    printf("%ld should equal %ld\n", temp, arr[0]);
}

While gcc happens to process this code correctly on 32-bit ARM (even if one uses flag -mcpu=cortex-m3 to avoid the call to memmove), clang processes it incorrectly on both platforms. Interestingly, while clang makes no attempt to reload p[index], gcc does reload it on both platforms, but the code for test on x64 is:

test(long*, int, int, int):
        movsx   rsi, esi
        movsx   rdx, edx
        lea     rax, [rdi+rsi*8]
        mov     QWORD PTR [rax], 1
        mov     rax, QWORD PTR [rax]
        mov     QWORD PTR [rdi+rdx*8], 2
        ret

This code writes the value 1 to p[index1], then reads p[index1], stores 2 to p[index2], and returns the value just read from p[index1].

It's possible that memmove will scrub active/effective type on all implementations that correctly handle all of the corner cases mandated by the Standards, but it's not necessary on the -fno-strict-aliasing dialects of clang and gcc, and it's insufficient on the -fstrict-aliasing dialects processed by those compilers.

supercat
  • 77,689
  • 9
  • 166
  • 211