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
- where it violates the standard and if it doesn't
- 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:
- Call
getDataN<100>(static_cast<void*>(&d));
andgetData4(static_cast<Base*>(&d));
for an instanceDerived<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); }
- call
getMemN<100>(static_cast<void*>(&d));
/getMem5(static_cast<Data*>(&d));
for anData<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); }
- 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;