TL;DR: first part of the answer contains an issue due to the switch between 32 and 64 bits (thanks to @PasserBy for its valuable comments). An updated snippet is provided at the end.
It is indeed UB.
Beyond the magic numbers for size and alignment (which are more a design issue) you have:
alignas(8) char localValue[16] = {42};
At this point only an array of initialized char exists.
Then
uint32_t* getAsVector() {
if (bytes <= 16) {
return reinterpret_cast<uint32_t*>(this->localValue);
}...
This implicitely creates (see below about implicit lifetime types) an array of std::uint32_t at the storage location but it's value maybe still be indeterminate. Indeed, if called before any set call only char objects have been initialized. Evaluation of indeterminate value is UB (see https://timsong-cpp.github.io/cppwp/n4950/basic.indet#2, thanks to @user17732522).
Besides, the correct way to tell the compiler that it is actually looking at an object of a different type is with std::launder. Notice that this does not help starting object lifetime, it only tells that we are effectively pointing to an object that exists (it helps the compiler to perform better and more accurate optimizations):
... std::launder(reinterpret_cast<uint32_t*>(this->localValue));
Even if you use your setter:
void set(uint64_t value) {
uint64_t* view = reinterpret_cast<uint64_t*>(this->localValue);
view[0] = value;
}
This implicitely creates an array of std::uint64_t at the storage location and you're initializing its value (yet the std::launder is also missing).
But the issue is that, in the getter, you're looking for std::uint32_t, not std::uint64_t and you initialized none.
Besides, when switching between one type of array and the other, your also triggering UB by trying to reference objects of unrelated types at the same location. It's just not allowed. I've opened another question to check if it possible to "convey" a value representation from a type to another one.
The only ways to map a byte-level value representation would be explicitly with calls to std::memcpy, std::bit_cast or std::start_lifetime_as for instance (see below) but they are some pitfalls too.
<original imperfect answer, improved below>
The correct way to go would be
alignas(std::uint64_t) alignas(std::uint32_t) char localValue[2*sizeof(std::uint64_t)];
(If I'm getting your intent right).
Then, in setter:
std::uint64_t* view = std::launder(reinterpret_cast<uint64_t*>(this->localValue));
provided that std::uint64_t is indeed implemented as a scalar type (which should be and can be check with is_scalar).
Why is it so: scalar type are implicit lifetime types which means, basically, that they are implicitly created (starting their lifetime) with some operations such as the creation of an array of bytes.
Besides, if I get it write, the std::uint64_t will live as long as its storage (third bullet of end of lifetime).
the std::launder is necessary to explicit to the compiler that an std::uint64 _t effectively exists at the given address.
Also, in getter:
std::uint32_t array[4] = std::launder(reinterpret_cast<uint32_t*>(this->localValue)); // actual size depends on the need but must match storage size
return array;
The same arguments as above apply (an array is also implicit lifetime type).
Another issue with your code is that you may have an indeterminate value when getting the buffer as an std::uint32 _t without having usedset beforehand, because nothing guarantees that the value representation will be kept.
If you want 42 to be usable as a default initialisation value, you may need a std::start_lifetime_as (c++23) to keep the value representation from the initialization. In this case, it will work because value representation of integers is well defined in c++. It won't be so straightforward for other types.
This also validate the switching between array of 32 bits and single 64 bits.
TWIST: to this day, std::start_lifetime_as is not supported: https://en.cppreference.com/w/cpp/compiler_support/23
As a work around you can initialize it with a constexpr static version of set. Maybe a lambda will do also.
<the issue in this answer is that I set 64 bits integer and read uninitialized 32 bits ones>
</original imperfect answer, improved below>
This is the improved answer:
#include <array>
#include <cstddef>
#include <cstdint>
#include <new>
struct S {
alignas(std::uint64_t) alignas(std::uint32_t)
std::array<std::byte, sizeof(std::uint64_t)> localValue = []() {
std::array<std::byte, sizeof(std::uint64_t)> init;
std::uint32_t* val =
std::launder(reinterpret_cast<std::uint32_t*>(init.data()));
val[0] = 0;
val[1] = 42;
return init;
}();
void set(uint64_t value) {
std::uint32_t* view =
std::launder(reinterpret_cast<std::uint32_t*>(localValue.data()));
// not proud of this but well defined IMHO :)
view[0] = static_cast<std::uint32_t>(value >> 8u*sizeof(std::uint32_t));
view[1] = static_cast<std::uint32_t>((value << 8u*sizeof(std::uint32_t)) >>
8u*sizeof(std::uint32_t));
}
uint32_t* getAsVector() {
return std::launder(
reinterpret_cast<std::uint32_t*>(localValue.data()));
}
};
LIVE
In this version arrays of 32 bits integer are guaranteed to live on every paths. Only 32 bits value representation are set.
memcpy. The compiler knows about this and will optimize it. See stackoverflow.com/a/70138631/17398063