1

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?

33
  • 1
    You are explicitly permitted to examine and modify an object via a pointer to char. It would be a rather meaningless permission if changes weren't guaranteed to propagate immediately. Commented Nov 24 at 8:38
  • 2
    Your screenshot says that the lifetime ends when the new object isn't nested in the old one. If you change the array elem type to unsigned char (as you have on the screenshot), then the array will be able to "provide storage" for the new object (per eel.is/c++draft/intro.object#3), so the new object will be nested in the array (eel.is/c++draft/intro.object#4.2). So while your snippet in the question could be undefined, the one on the screenshot looks legal, because they use unsigned char[]. Commented Nov 24 at 8:58
  • 2
    The behavior is undefined. A compiler can place the value p->x in a register and lose the changed value via buf[4]. Commented Nov 24 at 9:12
  • 1
    Realistically, this code is good enough and should work in practice. (It would be even safer if you used reinterpret_cast<unsigned char *>(p) instead of the original array, IMO.) The wording regarding object representation has some nice defects, so while it can be interesting to dig into, I doubt it's worth the time. Commented Nov 24 at 9:21
  • 3
    "in EDIT1 section I mean "S object lifetime ended", but not "buf lifetime ended". S object is still in its lifetime (no call to delete). Only p lifetime has ended. Commented Nov 24 at 13:51

1 Answer 1

4

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);
}

LIVE

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:

  • I am restricting it to trivially copyable types, but I do not see the point of low level value manipulation for more complex types;
  • I am wrapping the array of bytes inside a struct as I do not no a way to directly use 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).
Sign up to request clarification or add additional context in comments.

2 Comments

And modifying buf through p is something that is also not covered by C++ standard. In EDIT1 it is not guaranteed that buf[4] == 'a' at the last line, right?
The aim of eel.is/c++draft/basic.lval#11.3 was to allow this but it is "bugged" and it's the goal of P1839. Actually it is not to modify a byte array through an object allocated upon it, but to read the object representation of the said object (though the effect, in fine, is the same). Btw, I think that, technically, if you can read this representation, the effect should be symmetrical (modifying it should modify the object, in the limit of forming a valid value representation). I can't see what we can gain in terms of possible compiler optimization, for instance, by not all

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.