0

first a word in advance: The following code should not be used as it is and is just the condense of working code to the critical point. The question is only where does the following code violate the standard (C++17, but C++20 is also fine) and if it doesn't whether the standard guarantees the "correct output"? It is not an example for beginners how to write code or anything like that, it is purely a question about the standard. (On request via pm: alternative version further below)

Assume for the following that the class Base is never directly instantiated, but only via Derived<Size> for some std::size_t Size. Otherwise the undefined behaviour is obvious.

#include <cstddef>
struct Header
{ const std::size_t m_size; /* more stuff, remains standard layout */ };

struct alignas(Header) Base
{  
   std::size_t getCapacity()
   { return getHeader().m_size; }

   std::byte *getBufferBegin() {
     // Allowed by [basic.lval] (11.8)
     return reinterpret_cast<std::byte *>(this);

     // Does this give the same as the following code (which has to be commented out as Size is unknown):
     // //  Assume this "is actually an instance of Derived<Size>" for some Size, then
     // //  [expr.static.cast]-11 allows
     // Derived<Size> * me_p = static_cast<Derived<Size> *>(this);
     // // [basic.compound].4 + 4.3: say that
     // // instances of standard-layout types and its first member are pointer-interconvertible:
     // Derived<Size>::memory_type * data_p = reinterpret_cast<memory_type *>(me_p);
     // Derived<Size>::memory_type & data = *data_p;
     // // Degregation from array to pointer is allowed
     // std::byte * begin_p = static_cast<std::byte *>(data);
     // return begin_p;
   }

   std::byte * getDataMemory(int idx)
   {
       // For 0 <= idx < "Size", this is guaranteed to be valid pointer arithmetic
       return getBufferBegin() + sizeof(Header) + idx * sizeof(int);
   }

   Header & getHeader()
   {
      // This is one of the two purposes of launder (see Derived::Derived for the in-place new)
      return *std::launder(reinterpret_cast<Header *>(getBufferBegin()));
   }

   int & getData(int idx)
   {
      // This is one of the two purposes of launder (see Derived::Derived for the in-place new)
      return *std::launder(reinterpret_cast<int*>(getDataMemory(idx)));
   }
};

template<std::size_t Size>
struct Derived : Base
{
   Derived() {
      new (Base::getBufferBegin()) Header { Size };
      for(int idx = 0; idx < Size; ++idx)
         new (Base::getDataMemory(idx)) int;
   }

   ~Derived() {
      // As Header (and int) are trivial types, no need to call the destructors here
      // as there lifetime ends with the lifetime of their memory, but we could call them here
   }

   using memory_type = std::byte[sizeof(Header) + Size * sizeof(int)];
   memory_type data;
};

The question is not whether the code is nice, not whether you should do this, and not whether it will work in every single or any specific compiler - and please also forget alignment/padding for absurd compilers ;). Thus, please do not comment on style, whether one should do this, on missing const etc or what to take care of when generalizing that (padding, alignment etc), but only

  1. where it violates the standard and if it doesn't
  2. is it guaranteed to work (ie. getBufferBegin returns the begin of the buffer)

Please be so kind to refer to the standard for any answer!

Thanks a lot

Chris


Alternative

Edited: Both equivalent, answer what ever you like more... As there seems quite a lot of misunderstanding and nobody reading explaining comments :-/, let me "rephrase" the code in an alternative version containing the same questions. In three steps:

  1. Call getDataN<100>(static_cast<void*>(&d)); and getData4(static_cast<Base*>(&d)); for an instance Derived<100> d
struct Data { /* ... remains standard layout, not empty */ };
struct alignas(Data) Base {};
template<std::size_t Size>
struct Derived { Data d; };

// Definitiv valid
template<std::size_t Size>
Data * getData1a(void * ptr)
{ return static_cast<Derived<Size>*>(ptr)->d; }

template<std::size_t Size>
Data * getData1b(Base * ptr)
{ return static_cast<Derived<Size>*>(ptr)->d; }

// Also valid: First element in standard layout
template<std::size_t Size>
Data * getData2(void * ptr)
{ return reinterpret_cast<Data *>(static_cast<Derived<Size>*>(ptr)); }

// Valid?
Data * getData3(void * ptr)
{ return reinterpret_cast<Data *>(ptr); }

// Valid?
Data * getData4(Base* ptr)
{ return reinterpret_cast<Data *>(ptr); }
  1. call getMemN<100>(static_cast<void*>(&d));/getMem5(static_cast<Data*>(&d)); for an Data<100> d
template<std::size_t Size>
using Memory = std::byte data[Size];
template<std::size_t Size>
struct Data { Memory data; };

template<std::size_t Size>
std::byte *getMem1(void * ptr)
{ return &(static_cast<Data[Size]*>(ptr)->data[0]); }

// Also valid: First element in standard layout
template<std::size_t Size>
std::byte *getMem2(void * ptr)
{ return std::begin(*reinterpret_cast<Memory *>(static_cast<Data[Size]*>(ptr))); }

template<std::size_t Size>
std::byte *getMem3(void * ptr)
{ return static_cast<std::byte*>(*reinterpret_cast<Memory *>(static_cast<Data[Size]*>(ptr))); }

template<std::size_t Size>
std::byte *getMem4(void * ptr)
{ return *reinterpret_cast<std::byte**>(ptr); }

std::byte *getMem4(Data * ptr)
{ return *reinterpret_cast<std::byte**>(ptr); }
  1. the trivial
std::byte data[100];
new (std::begin(data)) std::int32_t{1};
new (std::begin(data) + 4) std::int32_t{2};
// ...
std::launder(reinterpret_cast<std::int32_t*>(std::begin(data))) = 3;
std::launder(reinterpret_cast<std::int32_t*>(std::begin(data) + 4)) = 4;
std::launder(reinterpret_cast<std::int32_t*>(std::begin(data))) = 5;
std::launder(reinterpret_cast<std::int32_t*>(std::begin(data) + 4)) = 6;
Chris N
  • 53
  • 7
  • 1
    So all of this mambo-jambo is used to convert `Base*` to `Header*`? If so then you can't do it. These types are unrelated. I'm also not sure what you are trying to use `launder` for. – ixSci May 23 '22 at 13:40
  • Please reduce your code to a [mcve]. At the moment most of it seems irrelevant for the purpose of this question. However, it would seem that @ixSci is right. – Konrad Rudolph May 23 '22 at 13:49
  • Why not simply use: `struct Derived : Header { std::array data; }` and work with a simple aggregate type? – Goswin von Brederlow May 23 '22 at 14:13
  • @ixSci: That's not correct. These are standard layout classes. A standard layout class can be cast to its first member. This first member being an byte array, Derived* can be cast to a pointer to its std::byte-array, which in turn can be cast to a pointer to its first element which (using the launder) can be cast to the type stored in the buffer (as this was created via inplace new). (see explanation in getBufferBegin) – Chris N May 23 '22 at 14:52
  • @GoswinvonBrederlow Because that would not be a standard layout class, disallowing most of the stuff + breaking ipc compatibility to other compilers. – Chris N May 23 '22 at 14:53
  • @KonradRudolph I think I cannot remove any more code as the function use the casts in question, the derived class defines the memory layout (being quite important for the question). :-/ – Chris N May 23 '22 at 14:55
  • @ChrisN yes, but your `Base` has no members so you have nothing to cast to. – ixSci May 23 '22 at 15:17
  • Regardless it is standard layout or not, there are multiple implementation, platform-defined issues. It MAY work. But it's not defined. It would be unspecified if you make a copy with memcpy, and that is allowed by standard layout and triviality, as it works through `char*`. Derived not related to Header. If you would placement-new a header inside of char\byte array , not int buffer, you'll have a case of "memory pool". But here you never created a Header. – Swift - Friday Pie May 23 '22 at 15:22
  • "A standard layout class can be cast to its first member." You are not casting a standard layout class nor are you casting to it's first member. You are casting the `std::byte[]` of the derived class to `Header&` and `int&`. In fact you are completely building your own layout out of raw memory. (well, via `this` pointer but since Derived is standard layout type that is the address of the `byte` array). – Goswin von Brederlow May 23 '22 at 15:27
  • More of , you cannot reuse original pointer after using std::launder on it. Result of std::launder shouldn't be ditched, like result of new-expression shouldn't be. It's an error. Could be fixed by using static. And class relations can be fixed by CRTP. – Swift - Friday Pie May 23 '22 at 15:35
  • `std::bit_cast` is specific for this sort of thing so long as you can guarantee a bit for bit matching. – doug May 23 '22 at 16:09
  • @doug yes I know, but C++17... – Chris N May 23 '22 at 17:52
  • 1
    @Swift I do not ditch the result of std::launder, I am returning it. Looking at https://en.cppreference.com/w/cpp/utility/launder, the "typical use case" for std::launder is: "Obtaining a pointer to an object created by placement new from a pointer to an object providing storage for that object." which is what I am doing in the lines with std::launder – Chris N May 23 '22 at 17:58
  • @Swift: Please look at the comments I gave in getBufferBegin(). There the chain of casts, I believe I am allowed to do is explained. In particular an instance of Derived is cast to the array of std::byte being its first (and only) member. Afterwards the "layout of raw memory" in data is used - the reason for that one was ditched when condensing a multiple hundred lines of code to this short example – Chris N May 23 '22 at 18:01
  • @Swift (sorry missed one point of your comment) And in the standard, there is nothing about pointers to the object providing the memory become invalid, you are just not allowed to dereference them (which I don't) as there lifetime ends (not that this is not true for a std::buffer array as it still contains the accessable pointer to the raw data of the type). You can call arbitrary many std::launder on the same memory-pointer to retrieve again and again the same typed pointer - the main reason for std::launder (at least once). – Chris N May 23 '22 at 18:05
  • Looks safe to me. The standard [appears to broken](https://stackoverflow.com/q/62329008/2752075) in this regard, but I'm certain that it won't break in practice. – HolyBlackCat May 23 '22 at 18:48
  • @HolyBlackCat Yes, exactly this is why I am not using members Header and std::byte[...] in Derived, but a pure data blob, as offsetof has this "bug"... – Chris N May 23 '22 at 18:55
  • I believe `getData3` & `getData4` aren't valid, I don't see what clause in Standard would allow it. I also think that `std::launder` does nothing in your example and can be omitted. – ixSci May 23 '22 at 19:28
  • launder isn't a magical function. It does nothing. It's a barrier to prevent pointer optimization, i.e. when compiler would consider that you did nothing or you reached into a void. If you are not allowed to reach some location using its argument, you are not allowed to reach it using launder (THAT is in standard) via returned type. But problem is, the `this` pointer that was used for launder is not same pointer , the storage is outside of Base's boundary. That can be fixed if `getBufferBegin` would be a a recurrent call (to derived class or via cast to derived class). – Swift - Friday Pie May 23 '22 at 19:33
  • @Swift-FridayPie Yes, obviously `std::launder` is a no-op function and therefore no magic function. If the data reachable by the returned type is not larger than the one by the type of the given pointer, then it is fine to use it. And the data is the same as there is a third type (std::derived<...>) which is pointer interconvertible with the given argument such that the binary representation of this third type contains the bytes accessed by the returned one. See answer I will give in a few minutes (will take a moment to write). – Chris N May 24 '22 at 08:30
  • @aschepler Which is the whole reason for the question... – Chris N May 24 '22 at 14:59
  • Oh, I misread the parts about "if it doesn't violate, is the desired behavior guaranteed" as "if it does violate, is desired behavior guaranteed anyway". Never mind that then. – aschepler May 24 '22 at 16:27

1 Answers1

1

Still unclear

The argumentation below that a base class of a standard layout class is pointer-interconvertible to a derived class is incorrect. More precisely, it holds only if the derived class wouldn't have any member (including member of a base class). Therefore, the strange discussion using C is not working as C inherits the members of Derived and calls them members of C.

As Base and Derived are not pointer interconvertible, the usage of std::launder to access to the data of Derived (see below) is against the standard as the object representation of Derived is not accessible from the pointer to the Base instance. So even if a pointer to Base has the same value as a pointer to Derived, the access via Base::getHeader would not necessarily be defined behaviour - probably undefined behaviour as there is no reason to think otherwise. Note: The compiler cannot assume that this data is not accessed via a Base pointer, as the data is accessible after an static_cast to Derived and therefore no optimization may be applied to this data. However, it remains that it is undefined behaviour if you used an reinterpret_cast (even if the value of the pointer is the same).

Question: Is there anything in the standard enforcing that a pointer to Derived is also a pointer to Base? They explicitly might have the same address, but are they guaranteed to? (at least for standard layout...). Or put differently, is reinterpret_cast<Base*>(&d) for a Derived d a well-defined pointer to the base subobject? (Regardless of accessibility)

PS: With C++20, we have std::is_pointer_interconvertible_base_of with which we can check, whether it holds for the given types.


Old answer

Yes, the presented code is both well-defined and behaves as expected. Let us look at the critical methods Base::getBufferBegin, Base::getData, and Base::getHeader one by one.

Base::getBufferBegin

First let us show a sequence of well-defined casts which will make the requested cast from the this pointer to a pointer to the first element in the array data in the Derived instance. And then secondly, show that the given reinterpret_cast is well-defined and gives the right result. For simplification, forget about member functions as a first step.

using memory_type = std::byte[100];
Derived<100> & derived = /* what ever */;
Base * b_p {&derived}; // Definition of this, when calling a member function of base.

// 1) Cast to pointer to child: [expr.static.cast]-11
// "A prvalue of type “pointer to B”, where B is a class type, can be converted to a prvalue
// of type “pointer to D”, where D is a class derived(Clause 13) from B."
// allowing for B=Base, D=Derived<100>
auto * d_p = static_cast<Derived<100> *>(b_p);

// 3. Cast to first member (memory_type ) is valid and does not change the value
// [basic.compound].4 + 4.3: -> standard-layout and first member are
// pointer-interconvertible, so the following is valid:
memory_type * data_p = reinterpret_cast<memory_type *>(d_p);

// 4. Cast to pointer to first element is valid and does not change the value
// [dcl.array].1 "An object of array type contains a contiguously allocated non-empty set of
// N subobjects of type T."
// [intro.object].8 "Unless an object is a bit - field or a base class subobject of zero
// size, the address of that object is the address of the first byte it occupies."
// [expr.sizeof]. "When applied to an array, the result is the total number of bytes in the
// array. This implies that the size of an array of n elements is n times the size of an
// element." Thus, casting to the binary representation (by [basic.lval].11 always allowed!)
std::byte * begin_p = reinterpret_cast<std::byte *>(data_p); // Note: pointer to array!
// results in the same as std::byte * begin_p = std::begin(*data_p)

A reinterpret_cast does not change the value of the given pointer², so if the first cast can be replaced by an reinterpret_cast without changing the resulting value, then the result of the above gives the same value as std::byte * begin_p = reinterpret_cast<std::byte *>(b_p);

[basic.compound].4 + 4.3 says (rephrased) Pointer to a instance of a standard-layout class without members is pointer-interconvertible has same address as any of its base classes. Thus, if C would be a standard layout child class of Derived<100>, then a pointer to an instance of C would be pointer-interconvertible to the a pointer to the sub-object Derived<100> and to one to the sub-object Base. By transitivity of pointer-interconvertibility ([basic.compound].4.4) a pointer to Base is pointer-interconvertible to a pointer to Derived<100> if such a class existed. Either we define C<Size> to be such a class and use C<100> instead of Derived<Size> or be just accept that it is not predictable from any object file whether there could be such a class C, so the only way to ensure this is that these two are pointer-interconvertible regardless of such a class C (and its existence). In particular, auto * d_p = reinterpret_cast<Derived<100> *>(b_p); can be used instead of the static_cast. Missing: auto * d_p = reinterpret_cast<Derived<100> *>(b_p); can be used instead of the static_cast.

Last step for Base;;getBuferBegin, can we replace all the above by *reinterpret_cast<std::byte*>(b_p);. First of all, yes we are allowed to do this cast as casting to the binary representation (by [basic.lval].11) is always allowed and does not change the value of the pointer²! Secondly, this cast gives the same result as we just have shown that the casts above can all be replaced by reinterpret_casts (not changing the value²).

All in all this shows that Base::getBufferBegin() is well-defined and behaves as expected (the returned pointer points to the first element in the buffer data of the child class).

Base::getHeader

The constructor of Derived<Size> constructs a header instance at the first byte of the array data. By the above Base::getBufferBegin gives a pointer to exactly this byte. The question remains, whether we are allowed to access the header through this pointer. For simplicity, let me cite cppreference here (ensuring that the same is in the standard (but less understandable)): Citing the "notes" from there

Typical uses of std::launder include: [...] Obtaining a pointer to an object created by placement new from a pointer to an object providing storage for that object.

Which is exactly what we are doing here, so everything is fine, isn't it? No, not yet. Looking at the requirements of std::launder, we need that "every byte that would be reachable through the result is reachable through p [the given pointer]". But is this the case here? The answer is yes, it surprisingly is. By the above argumentation (just search for [basic.compound].4.4 ;)) gives that a pointer to Base is pointer-interconvertible to Derived<Size>. Per definition of reachability of a byte via an pointer, this means that the full binary representation of Derived<Size> is reachable by a pointer to Base (note that this is only true for standard layout classes!). Thus, reinterpret_cast<Header*>(this); gives a pointer to the Header-instance through which every byte of the binary representation of Header is reachable, satisfying the conditions of std::launder. Thus, std::launder (being a noop) results in a valid object pointer to header.

Missing: Binary representation of Derived reachable through point to Base (no static_cast usage!)

Do we need that std::launder? Yes, formally we do as this reinterpret_cast contains two casts between not pointer-interconvertible object, being (1) the pointer to the array and the pointer to its first element (which seems to be the most trivial one in the full discussion) and (2) the pointer to the binary representation of Header and the pointer to the object Header and the fact that header is standard layout does not change anything!

Base::getData

See Base::getHeader with the sole addition that we are allowed to do the given pointer arithmetic (for 0<=idx and idx<=Size) as the given pointer points to the first element of the array data and the full array data is reachable through the pointer (see above).

Done.

Why do you need this discussion

Certification of a compiler ensures that we can rely on it doing what the standard says (and nothing more). By the above, the standard says that we are allowed to do this stuff.

Why do you need this construction

Get a reference to a non-trivial container (eg list, map) to a static memory buffer without

  1. containing the size of the container in the type - otherwise, we had quite a lot of templates -,
  2. undefined behaviour,
  3. indirect usage of the storage by using a (possibly temporary) api-class storing a pointer to header and data, as this will shift the problem to the users of the container casts,
  4. user-defined casts applied by the user of the container class as they will raise a warning for users of our base library,
  5. staying standard layout (yes, this is only the case if Header and the type stored in the buffer are standard layout, too),
  6. without using pointers.

The latter two being needed as the structure is sent around via ipc.


²: Yes, reinterpret_cast of a pointer type to another pointer type does not change the value. Everybody assumes that, but it is also in the standard ([expr.static.cast].13):

Blockquote If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. (...) Otherwise, the pointer value is unchanged by the conversion.

That shows that static_cast<T*>(static_cast<void*>(u)) does not change the pointer and by [expr.reinterpret.cast].7 this is equivalent to the corresponding reinterpret_cast

Chris N
  • 53
  • 7
  • an unrelated question as it is related to data alignment and boundaries... why Base::getData uses `sizeof(int)`, is it a typo and it should be `sizeof(std::byte)`? It returns pointer to byte and in formulation like that it depends on platform and size of Header if that pointer returned is compatible with cast to a pointer to `int*`. – Swift - Friday Pie May 24 '22 at 11:53
  • Because: `Derived` puts integers in the data. So there are no simple bytes in there but integer (or you could template this stuff and put something else there - then you really have to take care of alignment etc.) And yes, here it is used that alignment of Header is stricter than the one of int and that sizeof(Header) must be a multiple of the alignment of int. – Chris N May 24 '22 at 12:11
  • 2
    In [basic.compound]/4, "has no non-static data members" includes any members of base classes, per [class.derived]/2. Otherwise it wouldn't make sense for C++20 to change pointer-interconvertible from C++17's "first base class" to "any base class" for memberless classes. Also, if your reasoning were valid then every standard-layout class would be pointer-interconvertible with its bases, and there would be no point to the "has no non-static data members" wording. – aschepler May 24 '22 at 13:13
  • @aschepler sadly it seems you are right. Let me think about it for a moment – Chris N May 24 '22 at 14:12
  • _"Or put differently, is reinterpret_cast(&d) for a Derived d a well-defined pointer to the base subobject?"_ Of course it is defined and you don't even need `reinterpret_cast` because it is a simple upcast (you don't need any cast but `static_cast` is enough if you insist). There is also [this](https://timsong-cpp.github.io/cppwp/n4868/class.mem#general-26) which answers your previous question. But I don't see how any of it might help your casting `Base*` to `Header*`. – ixSci May 25 '22 at 06:34
  • Nope, that does not answer the question. A simple upcast is a **`static_cast`**, not a `reinterpret_cast`! That's an important difference - as can easily be seen if there are more than one parent (in the sense of multi-inheritance) where the `static_cast` will (for one of the parents) change the value, ie. *not* be a `reinterpret_cast`. The link you gave is the one I am/was citing just proving that point for standard_layout types *without members*! So that doesn't help either. – Chris N May 25 '22 at 07:06
  • If you have different results with `reinterpret_cast` and `static_cast` then you are not allowed to use the result of the `reinterpret_cast` — it is UB (unless, of course, you are using char/byte but that is a completely different matter). I don't understand your comment regarding the link: it specifically says about the case *with members* and I don't see you citing it anywhere. And it says that they indeed have the same address which you asked for: _"They explicitly might have the same address, but are they guaranteed to?"_ – ixSci May 25 '22 at 07:43
  • 1
    _"With C++20, we have std::is_pointer_interconvertible_base_of"_ Don't get your hopes high. `std::is_standard_layout` isn't working properly in any compiler, so there is no reason to believe that that will. – ixSci May 25 '22 at 07:44
  • @ixSci hmm, what about `std::launder(reinterpret_cast
    (std::launder(reinterpret_cast(static_cast(this)))));` My impression, that to follow standard to a tee, one cannot skip steps in that. Intermediate cast it a cast that that array member, variable, are we allowed do so? We sadly, again hinge our hopes on that Header is zero sized. To be honest, use of trait class is more straightforward ideologically.
    – Swift - Friday Pie May 25 '22 at 07:48
  • @Swift-FridayPie why not just: `reinterpret_cast
    (static_cast(this));`? Given `B` is never created outside of `Derived` I don't see why this chain would be invalid. I also don't understand this obsession with `std::launder` it was introduced because of constant expressions and what the OP is trying to do was either possible or not possible long before `launder` was added.
    – ixSci May 25 '22 at 07:56
  • 1
    @Swift-FridayPie sorry, I forgot already that `Derived` has a byte array and not just a `Header` member so the chain I posted is invalid. – ixSci May 25 '22 at 08:04
  • @ixSci Yeah with header member or multiple non-virtual inheritance that'd worked. Until certain moment none of this was defined at all this way as there was no guarantee that `Base` must have size of zero for its address to match to array address- and some compilers actually added 1 to size of object as result of inheritance. It was related to mistreating the statement about a unique address of member within a class.. Yet I saw similar code relying on non-portable features with conditional compilation, i.e. within Qt framework – Swift - Friday Pie May 25 '22 at 08:07
  • @ixSci One also must be careful not to move hierarchy base. If `Base` with be derived from other class with non-zero size, it breaks. In older compiler it would break even if we add `sizeof(Base)` to address. When speaking about zero size I mean , size of subobject. As separate object `Base` always will be at least of size 1. – Swift - Friday Pie May 25 '22 at 08:12
  • @Swift-FridayPie in that case the class won't be standard-layout. To be SL a class should have all non-static members declared in one class. – ixSci May 25 '22 at 08:13
  • @Swift-FridayPie No, `Base` is defined to be an empty size subobject and this is guaranted (in contrast to if you instantiate `Base` directly then it has to use memory) – Chris N May 25 '22 at 08:17
  • @Swift-FridayPie _hmm, what about std::launder(reinterpret_cast
    (std::launder(reinterpret_cast(static_cast(this)))));_ As soon as you can use Derived, you have no problem. You can just make `std::launder(reinterpret_cast
    (static_cast(this)->m_data));` and be done (all that is allowed). But the main point is to note use `Derived` as it contains a template parameter you cannot know at `Base`
    – Chris N May 25 '22 at 08:20
  • @ixSci yes, but if that only one class will be the `Header`? Or some `Packet` which would contain `Header` and byte arrays as as members. – Swift - Friday Pie May 25 '22 at 08:20
  • @ChrisN that's a relatively new requirement, historically. – Swift - Friday Pie May 25 '22 at 08:21
  • If Derived would just be `struct Derived { Header header; std::byte data[...]; }` then the cast to data would be impossible as you are not allowed to run arround in the binary representation of a typ (see https://stackoverflow.com/questions/62329008/is-it-ub-to-access-a-member-by-casting-an-object-pointer-to-char-then-doing). That's the sole reason for all the above. And by the above there is a solution in C++20 (regardless of `bit_cast` being exactly what we need for the cited problem) – Chris N May 25 '22 at 08:22
  • _that's a relatively new requirement, historically._ Yes the guarantee for zero subobject to have zero size was needed for standard layout (actually afaIk exactly and only for this case...) Doesn't help with the problem here. – Chris N May 25 '22 at 08:24
  • Why is it not in the standard that a pointer to a base subobject points always to a point before/at the first member of the derived class (at least for standard layout!). That would resolve the above... **Everybody** believes it to be true. – Chris N May 25 '22 at 08:27
  • @ChrisN try to contact someone from the committee. Maybe it will become some DR and eventually be fixed. Or you will be told that everything is fine and your case isn't going to be supported. – ixSci May 25 '22 at 08:30
  • @ChrisN because virtual inheritance does exist. I'm more curious why more than single instance of same base class isn't allowed. I.e. `Derived` cannot contain a member of type `Base`, it makes it non-standard. They meant to describe cursed diamond multiple inheritance that way, but defined wider ban.It's not like the ban of spreading members across separate classes isn't already removing problems with diamond from memory layout point of view. - that's also a new requirement – Swift - Friday Pie May 25 '22 at 08:30
  • There was that answer to those frustrtations about standard layout https://stackoverflow.com/questions/7160901/why-is-c11s-pod-standard-layout-definition-the-way-it-is – Swift - Friday Pie May 25 '22 at 08:39
  • Yes, for virtual inheritance everything is quite obvious (as it is for multi-inheritance), but for standard layout? It is stupid. Even for virtual/multiple inhertance you could say that any *member* of any base class is before any of the derived class (not being one of the base class), you just couldn't enforce that for the vtable etc (not being a member!) Probably all of that is because some obscure, long outdated C++-compiler put the base class after the derived one, which is why this order didn't made it to the standard and nobody cared to change that (being afraid of breaking something) – Chris N May 25 '22 at 08:53
  • _And by the above there is a solution in C++20 (regardless of bit_cast being exactly what we need for the cited problem)_ No, actually bit_cast doesn't help us a bit (haha) here - no references in the result... – Chris N May 25 '22 at 09:19