2

I am trying to hide some implementation behind an opaque data type. Normally, pimpl would be the preferred pattern, but it requires heap allocation which is not desirable in this context (embedded, interrupt handler, high IQRL, etc.).

The way I saw this implemented before is to make the opaque type the right size so it can be kept on stack by the caller, then cast it to implementation:

// *.h
struct opaque_handle {
    alignas(max_align_t) char _storage[128];
};

void set_x(opaque_handle* handle, int x);
int get_x(const opaque_handle* handle);

// *.c
struct impl {
    int x;
};
static_assert(sizeof(struct impl) <= sizeof(struct opaque_handle)), "Insuficcient handle size");

void set_x(opaque_handle* handle, int x) {
    ((const struct impl*) handle)->x = x;
}

// user
int get_x(const opaque_handle* handle) {
    return ((const struct impl*) handle)->x;
}

// user
struct opaque_handle handle = { 0 };
set_x(&handle, 5);
assert(get_x(&handle) == 5);

But this involves type punning, which is UB. Is there a way to implement get_x in a compliant way?

6
  • Have you considered that hiding the details of your data structure might not be such a useful thing to do in the first place? What message are you trying to send by doing that that is not adequately conveyed by documentation (or lack thereof)? What functional advantage do you hope to obtain? And is that a good value proposition in exchange for the extra complexity? Commented Oct 31 at 13:44
  • @JohnBollinger Hyrum's law Commented Oct 31 at 14:19
  • "Hyrum's Law" is neither a message being conveyed nor a functional advantage. Nor a maintenance advantage. In fact, if you accept Hyrum's then it tells you that hiding the implementation of your data structure will lead to people reverse-engineering it and relying on the hidden layout. That's worse for pretty much everybody than just using a normal structure and documenting it with only an admonishment to treat it as opaque. Commented Oct 31 at 15:01
  • ... the article you linked even claims specifically, "Given enough use, there is no such thing as a private implementation," so Hyrum's seems a particularly poor justification for trying to hide your implementation. Commented Oct 31 at 15:18
  • It was meant as a joke. Yes, you cannot hide your implementation completely. But that doesn't mean you should not place signposts to indicate to users what they should and should not use. My usecase is providing scratch space for a state machine that implements a function. Caller should not mess with it, all they need to know is whether to call the function again and what resources should be locked while they do. Commented Oct 31 at 18:39

1 Answer 1

5

What you can do is using memcpy:

void set_x(opaque_handle* handle, int x) {
    struct impl tmp = { .x = x };
    memcpy(handle->_storage, &tmp, sizeof(tmp));
}

int get_x(const opaque_handle* handle) {
    struct impl tmp;
    memcpy(&tmp, handle->_storage, sizeof(tmp));
    return tmp.x;
}

impl is only defined in your implementation file (which I wasn't paying attention to in the first place),
which makes it safe to use.

memcpying is sufficient to avoid Undefined Behaviour here

Sign up to request clarification or add additional context in comments.

9 Comments

And in general, if there were more members, then set_x has to memcpy twice to preserve them, right?
Yes, you need to memcpy first from handle to tmp and then set tmp.x and then memcpy to the storage.
You can use offsetof to memcpy the argument directly into the array.
memcpy(&(handle->_storage[offsetof(struct impl, x)]), &x, sizeof(int))
I believe the use of a union might enable an implementation without memcpy() that doesn't invoke UB, but I don't have the time to dig into the details right now.
Not going to look it up but, I think, storing to one member of the union and then reading from the other is technically UB. Could be wrong though.
The library code would only read/write through the actual implementation, though. And the application would only be accessing it via a char array, which can alias anything without violating strict aliasing. So, maybe.
I don't think so. You cannot declare union { struct opaque_handle; struct impl; } in the header, because impl is not known. And declaring it inside the function means you have to memcpy opaque_handle into it anyway. Never mind that accessing inactive member is UB anyway.
If one uses a dialect that support Common Initial Sequence guarantees (which clang and gcc support when using -fno-strict-aliasing), then one can have a variety of types which share a common initial sequence of callback pointers, and have various "create thing" functions which return the address of objects in a static-duration array and populate their members suitably, without client code having to care about the exact types or the static allocation used for the pool.

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.