Consider the code below assuming 4 is a correct index of p->c.
Are buf and *p objects alive at the same time?
struct S { int x; char c; };
alignas(S) unsigned char buf[sizeof(S)];
S* p = new(buf) S{};
p->x = 10;
p->c = 'a';
std::cout << p->x << " " << buf[4] << '\n';
buf[4] = 'b';
std::cout << p->x << " " << p->c << '\n';
Is it guaranteed by C++ standard that this code will always print exactly the following (assuming 4 is a correct index)?
10 a
10 b
In other words, are the changes made to *p "immediately reflected" in buf and vise versa? Is it possible that
p->c = 'a';
does not actually result in the writing 'a' to buf, for example?
EDIT1:
What will happen if I change the code into this:
struct S { int x; char c; };
alignas(S) unsigned char buf[sizeof(S)];
{
S* p = new(buf) S{};
p->x = 10;
p->c = 'a';
// And what changes if I destroy S object?
// p->~S();
}
std::cout << buf[4] << '\n';
C++ standard allows to examine object's byte representation via unsigned char*, but there is probably no object when I access buf[4], because S object lifetime ended, right?
You are quoting in comment an answer to this similar question but I'm more convinced by the other answer.
The most upvoted answer is citing https://eel.is/c++draft/basic.lval#11 stating that type-accessibility is a necessary condition in order to access the stored value of an object but it is not a sufficient one.
In comment I added https://eel.is/c++draft/expr.static.cast#12 but you would need pointer-interconvertibility which is not achieved if you want to modify a value from its byte storage (see P1839).
In the linked answer above, Jan Schultke clearly explain that there is no guarantee that you can modify p from buf and he even goes farther pointing P1839#non-goals, stating that modification of an object through fiddling with its object representation as an array of bytes is not covered in the standard. Only reading is considered and P1839 aims at making it straight.
But if your goal is to do such a fiddling, there is a workaround through std::bit_cast:
#include <bit>
#include <cstddef>
#include <cstdint>
#include <type_traits>
template <typename T>
concept trivially_copyable = std::is_trivially_copyable_v<T>;
namespace details {
// required to pass a plain array to and from bit_cast
template <typename T, std::size_t N>
struct wrap_array {
T arr[N];
};
} // namespace details
template <trivially_copyable T>
constexpr void set_byte(T& obj, std::size_t pos, std::byte val) {
details::wrap_array<std::byte, sizeof(T)> obj_rep;
// static only with c++>=23
// static details::wrap_array<std::byte,sizeof(T)> obj_rep;
obj_rep = std::bit_cast<details::wrap_array<std::byte, sizeof(T)>, T>(obj);
if (pos >= sizeof(T)) {
return;
}
obj_rep.arr[pos] = val;
obj = std::bit_cast<T, details::wrap_array<std::byte, sizeof(T)>>(obj_rep);
}
template <trivially_copyable T>
[[nodiscard]] constexpr T set_byte(T const& obj, std::size_t pos,
std::byte val) {
details::wrap_array<std::byte, sizeof(T)> obj_rep;
// static only with c++>=23
// static details::wrap_array<std::byte,sizeof(T)> obj_rep;
obj_rep = std::bit_cast<details::wrap_array<std::byte, sizeof(T)>, T>(obj);
if (pos >= sizeof(T)) {
return obj;
}
obj_rep.arr[pos] = val;
return std::bit_cast<T, details::wrap_array<std::byte, sizeof(T)>>(obj_rep);
}
template <trivially_copyable T>
constexpr void set_byte_unsafe(T& obj, std::size_t pos, std::byte val) {
std::byte* ptr = reinterpret_cast<std::byte*>(&obj);
if (pos >= sizeof(T)) {
return;
}
ptr[pos] = val;
return;
}
// hides the real value from the compiler
std::uint32_t faucet();
// prevent the compiler from optimizing away a variable
void sink(std::uint32_t const&);
int main() {
std::uint32_t x{0};
x = faucet();
set_byte(x, 1, std::byte{255});
sink(x);
x = faucet();
set_byte_unsafe(x, 2, std::byte{255});
sink(x);
constexpr std::uint32_t y{0};
constexpr auto z = set_byte(y, 1, std::byte{255});
sink(z);
return static_cast<int>(x);
}
The non-void set_byte version is only there for having the possibility to initialize a constexpr variable with it.
What is interesting is to compare, on compiler explorer, the assembly produced in a optimized build. It is quite the same for the safe version and the unsafe one, which uses reinterpret_cast and might well be UB for different reasons (see many recent discussions, around your other question).
I am not aware of proposal to modify an object value from its object representation, perhaps because the compilers, so far, behave as expected (bad reason) and because you can do byte fiddling through bit_cast (good reason), hoping for compiler to optimize (bad reason, but not so bad). So your trading a hypothetical UB for a hypothetical miss-optimization.
Side-note about set_byte implementation:
std::bit_cast on plain array. But wrap_array object representation is guaranteed to be the same as the wrapped array (and at the same memory location).