Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it technically UB to static_cast<A*>(memmove(dst, (void*)src, sizeof(src))) since C++20?

Consider the following excerpt from wg21.link/p0593:

A call to memmove behaves as if it

  • copies the source storage to a temporary area

  • implicitly creates objects in the destination storage, and then

  • copies the temporary storage to the destination storage.

This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.

struct A { int n; };
auto a = A{1};
void* p1 = &a;
alignas(A) char buf[8];

// p1 is of void*, so it shouldn't be assumed pointing to an object of A.
auto p2 = static_cast<A*>(memmove(buf, p1, sizeof(A))); 
p2->n = 2; // Is this technically UB since C++20?

auto fn = [&] { return memmove(buf, p1, sizeof(A)); };
// Must it be `auto p3 = std::start_lifetime_as<A>(fn());` here?
auto p3 = static_cast<A*>(fn()); // fn is not blessed by the standard as memmove.
p3->n = 3; // Is this technically UB since C++20?
like image 379
xmllmx Avatar asked Sep 14 '25 12:09

xmllmx


1 Answers

This is UB in 3 levels:

  1. Even before C++20, char was not the proper element type for a void buffer. Either use unsigned char, or C++17 std::byte(<cstddef>) whose main use case is void buffer:
std::array
   < std::byte
   , 2 * sizeof(A)
> buffer {};
  1. The alignment is not guaranteed. It should either be statically, or dynamically specified:
alignas(A) std::array
   < std::byte
   , sizeof(A)
> static_align_buffer {};

std::array
   < std::byte
   , 2 * sizeof(A)
> buffer {};

auto volume = size(buffer);//mutable

auto dynamic_align_buffer
   = std::align
        ( alignof(A), sizeof(A)
        , data(buffer), volume );
  1. The result of std::memmove was not guaranteed to be an implicitly created before LWG4064. So, before C++26, you have to invoke C++23 std::start_lifetime_as<A> on some platforms, and static_cast<A*> on others, or just use the destination argument to std::memmove and std::launder:
A a {1};
std::memmove
   ( dynamic_align_buffer,
   , &a, sizeof(a) );

auto p2
   = std::launder
        ( static_cast<A*>
          ( dynamic_align_buffer ) );

But all the effort above is futile in presence of C++20 <bit>. It's considered unsafe and code smell to use either of std::memmove, std::memcopy, std::memset. These functions serve four fundamental algorithms in C which have well-optimized generic counterparts in C++. Because if the operand type is not trivial, the mem[*] functions result in resource management UB(double free, use after free, leak). The four use cases are:

  1. Copy container elements:
std::ranges::copy
   ( source 
     | std::views::take
          ( std::ranges::distance
               ( dest ) ) // take
   , std::ranges::begin(dest) );
  1. Move container elements:
std::ranges::move
   ( source 
     | std::views::take
          ( std::ranges::distance
               ( dest ) ) // take
   , std::ranges::begin(dest) );
  1. Fill containers elements:
std::ranges::fill(dest, value); 
  1. Type punning:
static_assert
(  sizeof(dest_t)
== sizeof(source)  );

dest_t dest
= std::bit_cast<dest_t>
     ( source );

There's no other general use case for mem[*] functions. These functions should only be invoked by the platform, not user or 3rd party libraries.

like image 81
Red.Wave Avatar answered Sep 17 '25 01:09

Red.Wave