这是铸造和STD :: Launder C++标准条件,没有不确定的行为

发布于 2025-01-31 21:17:33 字数 5667 浏览 1 评论 0原文

首先,提前一个单词:以下代码应原样使用 ,只是工作代码到关键点的凝结。这个问题仅是在哪里违反标准(C ++ 17,但C ++ 20也可以),如果不是标准是否保证“正确输出”?这不是初学者如何编写代码或类似内容的示例,而是关于标准的问题。 (根据请求通过PM:替代版本进一步)

假设以下内容base从不>一些std :: size_t size的派生< size> 。否则,不确定的行为是显而易见的。

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

问题是不是代码是否不错,不是您是否应该这样做,而不是 它是否在每个或任何特定编译器 - 也请忘记荒谬编译器的对齐/填充;)。因此,请对样式进行而不是 ,无论是一个应该做到这一点,在丢失const等上还是在概括时要处理什么(填充,对齐等),但仅

  1. 在违反标准
  2. 情况

的 任何答案的标准!

非常感谢

Chris


Selternative

编辑:同等的,请回答您更喜欢的东西... 由于似乎有很多误解,而且没有人阅读解释评论: - /,让我“重新塑造”包含相同问题的替代版本中的代码。在三个步骤中:

  1. 呼叫getDatan&lt; 100&gt;(static_cast&lt; void*&gt;(&amp; d)); and getData4(static_cast&lt; base*&gt; base* /代码>用于实例派生&lt; 100&gt; 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. 呼叫getMemn&lt; 100&gt;(static_cast&lt; void*&gt;(&amp; d));//getMem5(static_cast&lt; data; data; data*&gt;(amp; d));用于data&lt; 100&gt; 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. 微不足道
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;

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;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

dawn曙光 2025-02-07 21:17:33

仍然不清楚

下面的论点是,标准布局类的基类是指派生类的指针交流是不正确的。更确切地说,仅当派生类没有任何成员(包括基类的成员)时,它才能保持。因此,使用c的奇怪讨论无法用作c继承派生的成员,并将其称为c 。

由于base派生的不可互通>,因此用法std :: Launder访问派生>的数据>(请参见下文)是违反标准的,因为从指针到base实例的指针无法访问的对象表示。因此,即使 指向base的指针具有与指向派生的指针相同的值,请通过base :: getheader 不一定是定义的行为 - 可能是未定义的行为,因为没有理由其他思考。
注意:编译器不能假设该数据无法通过base指针访问,因为在static_cast
derived之后,可以访问数据不能对此数据应用任何优化。但是,如果您使用reinterpret_cast(即使,则指针的值是相同的),则仍然是未定义的行为。

问题:标准执行中是否有任何指向派生的指针也是base> base的指针?他们明确地可能有相同的地址,但是保证了吗? (至少对于标准布局...)。
或换句话说,是reinterpret_cast&lt; base*&gt;(&amp; d) derived d
定义明确的指针指向基本子对象? (不管可访问性如何)

PS:使用C ++ 20,我们具有std :: is_pointer_interconvertible_base_of我们可以检查它是否适用于给定类型。


旧答案

,提出的代码既明确定义,又是预期的。让我们查看关键方法base :: getBufferbeginbase :: getDatabase :: getheader一一。

base :: getBufferbegin

首先让我们显示一系列定义明确的铸件,这将使所请求的铸件从该指针转换为“数组” data 中的第一个元素的指针在派生实例中。其次,证明给定的reinterpret_cast是明确定义的,并给出正确的结果。为了简化,请忘记成员功能作为第一步。

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)

reinterpret_cast不更改给定指针的值,因此,如果可以用reinterpret_cast替换第一个铸件而不更改结果值,则上述结果给出与std :: byte * begin_p = reinterpret_cast&lt; std :: byte *&gt;(b_p);

[basic.compound] .4 + 4.3 say(重新分配) )指向没有成员的标准线路类实例的指针是指指针交换的地址与其基类的任何任何因此,如果c将是派生的标准布局子类,则是c实例的指针,将是可与a的指针交换指向sub-object 派生的指针,并将其指向sub-object base。通过指针交换性的传递性([basic.compound] .4.4)指向base的指针是指向指向派生的指针的指针交换代码>如果存在这样的类。我们要么定义c&lt; size&gt;是这样的类,并使用c&lt; 100&gt;而不是derived&lt; size&gt;,或者只是接受是否可以从任何对象文件中预测,是否可以有这样的类c,因此,确保这两个是可相互交流的唯一方法,无论此类类别c c (及其存在)。特别是auto * d_p = reinterpret_cast&lt; derived&lt; 100&gt; *&gt;(b_p);可以使用static_cast
缺少:auto * d_p = reinterpret_cast&lt; derived&lt; 100&gt; *&gt;(b_p);可以使用static_cast

base> base> base ;; GetBuferbegin的最后一步*reinterpret_cast&lt; std :: byte*&gt;(b_p);。首先,是的,我们被允许将其施放到二进制表示形式(由[BASIC.LVAL] .11)中,并且总是允许,并且不会更改指针的价值!其次,此铸件给出了相同的结果,因为我们只是表明上面的铸件都可以用reinterpret_cast s(不更改值²)代替。

总而言之,所有这些都表明base :: getBufferbegin()是定义明确的,并且行为预期(返回的指针指向子类的缓冲区数据中的第一个元素)。

base :: getheader

派生的构造函数在数组数据的第一个字节上构造了标头实例。 通过上述base :: getBufferbegin给出了一个准确的指针。问题仍然存在,是否允许我们通过此指针访问标头。为简单起见,让我引用 cppreference 在这里(确保在标准(但不太可理解)):引用从那里引用“笔记”

std ::洗劫的典型用途包括:[...]从位置新的对象中获取从指针到提供该对象存储的对象创建的对象的指针。

我们在这里所做的那是什么,所以一切都很好,不是吗?不,还没有。查看std ::清洗的要求,我们需要“通过P [给定的指针]可以达到结果的每个字节都可以达到结果”。但是这里是这样吗? 答案是,令人惊讶的是。通过上述论点(只需搜索[basic.compound] .4.4;))给出一个指向base的指针是指对于derived&lt; size&gt;的指针交换。根据通过指针的字节可及性的定义,这意味着派生的完整二进制表示形式可通过指向base> base的指针来触发对于标准布局类别!)。因此,reinterpret_cast&lt; header*&gt;(this);给出了指向header instance的指针,headser >是可以达到的,满足std :: Launder的条件。因此,std :: Launder(是NOOP)导致有效的对象指针指向标头。

缺失:的二进制表示可通过点到达到base(否static_cast usage!)

我们是否需要std :: Launder?是的,我们正式这样做是这样做的reinterpret_cast包含在不指针交换对象之间的两个铸件,是(1)指向数组的指针和指向其第一个元素的指针(这似乎是最琐碎的一个在完整的讨论中)和(2)指向header的二进制表示的指针和对象header的指针,以及标准布局标准布局不会更改任何内容的事实呢

base :: getData

请参阅base :: getheader,唯一允许我们执行给定的指针算术(对于0&lt; = IDXidx&lt; = size)作为给定指针指向数组的第一个元素data和完整的数组 data 可通过指针达到(请参见上文)。

完毕。

为什么您需要

对编译器的讨论认证确保我们可以依靠它来执行标准所说的内容(仅此而已)。根据上述标准,标准说我们可以做这些事情。

为什么您需要此构造

会引用非平凡容器(例如,地图)到静态内存缓冲区,而不

  1. 包含该类型中的容器大小 - 否则,我们有很多模板 -
  2. 未定义的行为,
  3. 通过使用(可能是临时的)API类对存储的间接用法存储指向标头和数据的指针,因为这将使问题转移到容器铸件的用户,
  4. 用户定义的铸件由容器类的用户应用因为他们将向我们的基本库的用户提出警告,因此
  5. 请保持标准布局(是的,只有header,而存储在缓冲区中的类型也是标准布局的情况),
  6. 而无需使用Pointers 。

由于结构是通过IPC发送的,因此需要后两个。


²:是的,reinterpret_cast针对其他指针类型的指针类型不会更改值。每个人都假设,但它也是在标准中([expr.static.cast] .13):

blockQuote如果原始指针值表示内存中字节的地址a,并且A不满足t的对齐要求,则未指定结果指针值。 (...)否则,指针值不变。

这表明static_cast&lt; t*&gt;(static_cast&lt; void*&gt;(u))不更改指针,并且通过[expr.reinterpret.cast] .7这等同于相应的<<代码> reinterpret_cast

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文