In C++, is this legal?

struct sigma { void *ptr; size_t size; }; class ligma { sigma sigma_; public: // a bunch of non-virtual methods here }; std::span<sigma> convert(std::span<ligma> buf) { return { (sigma*)buf.data(), buf.size() }; }

As I understand this, sigma and ligma will have the exact same layout (the only reason ligma exists is to hide implementation details of sigma and maybe add some convenience methods & constructors), and it should be legal in terms of strict aliasing stuff to convert from ligma pointer to sigma pointer. However arrays are a different beast. I can’t seem to find any information on whether or not this type of conversion is allowed in the language or if it’s undefined behavior in some super obscure way.

#cpp #cplusplus

@kimapr you can reinterpret_cast between pointers to `pointer-interconvertible` types.
If ligma is a standard-layout class, you can cast a pointer to ligma object to pointer to it's first member's type.
[basic.compound] in standard spec
You should check the requirements for standard layout and see if you can satisfy them

@deezo

If ligma is a standard-layout class, you can cast a pointer to ligma object to pointer to it’s first member’s type.

This is basically what i already know, but sadly is not enough for my case, even though i am sure ligma object is standard layout. I need to not just convert ligma pointer to sigma (which is first member of ligma) pointer, but convert pointer to an array of ligma objects to pointer to an array of sigma objects.

in the general case this can’t be legal, because the “nesting” object could contain extra members compared to its first member which would throw off array alignment completely.

my ligma object however for sure does not do this, it’s only member is the sigma object, so an array of ligmas would have the exact same physical layout as an array of sigmas.

when i convert array pointers like this the code works as i expect with no issues, but that doesn’t mean it lacks undefined behavior which is the concern here and why i’m asking.

@kimapr
I misunderstood the initial question, sorry about that.
I do think it's technically UB for the case with arrays. I think the relevant part is 7.6.6 Additive operators. Pointer addition is defined for pointer P if it is a pointer to an array element. Since there's no array of sigmas, it's UB.
I'm not a standard expert, I am studying it myself, please verify :)
Do you need the `std::span` specifically? Some range adaptor would be legal and likely as performant with optimizations enabled.
@kimapr Reading into it more carefully, I still think it's UB, but my initial reasoning was incorrect.
An object can be treated as an array of size 1.
But adding any integral value j to the pointer to i'th element that will result in (i+j) outside of array bounds of the array is UB (even without access).
So you can only create span<sigma> of size 1 from ligma.

@deezo

Do you need the std::span specifically? Some range adaptor would be legal and likely as performant with optimizations enabled.

i’m making an API function for a vectored write based on libuv, and libuv’s own APIs, including structs, are supposed to be hidden. The idea here is that:

  • libuv has a struct type uv_buf_t (that’s the sigma here), and libuv’s vectored write function accepts a pointer to an array of those.
  • user of my API instead constructs an array of my class let’s call it IoVec (the ligma in the example), and passes a span of those to my function, which then turns it into a pointer to an uv_buf_t array to pass it to libuv, importantly without making any extra allocations (otherwise it would be less annoying to just have my func accept a span<span<char>> instead)

some fancy adaptor would likely overcomplicate things. maybe having user pass an array of uv_buf_ts directly could work, but ehhhh

i got curious and looked into how libuv itself does it, after all it’s getting a uv_buf_t array pointer and needs to pass the libc type struct iovec to writev, and those are, too, different types. and oh my god. it just casts the uv_buf_t* to struct iovec*!! the structs don’t even have the same members. iovec has a void *iov_base as the first member, uv_buf_t has char *base first member, it’s so blatantly wrong. The devs literally couldn’t care less about UB, just “oh its the same bits and bytes on the memory who cares”

Maybe if the library i am using is so careless about UB, i don’t really need to care about it too much in the program that uses the library either. after all the cat is kind of out of its bag already. Though thinking about it, both libuv’s UB and my UB would be very unlikely to ever cause any issues, as the actual invalid access is occuring across a translation unit boundary. just gotta be careful with LTO :-)

@kimapr I think another factor is that C might be more forgiving when casting stuff.
@deezo i’m pretty sure it’s illegal even in C though.. C does allow some things that C++ doesn’t like type punning using unions but it does still have rules, you can’t just cast random pointers left and right

@kimapr
> cast random pointers left and right

TBH that's how I see C programming in a nutshell 😂