这种铸造和 std::launder 是否符合标准C++并且没有未定义的行为



首先提前一个词:下面的代码不应该按原样使用,只是工作代码到临界点的浓缩。问题只是以下代码在哪里违反了标准(C++17,但C++20也可以),如果没有,标准是否保证"正确的输出"?这不是初学者如何编写代码或类似内容的示例,这纯粹是关于标准的问题。(通过下午要求:替代版本在下面进一步)

假设以下类Base永远不会直接实例化,而只是通过某些std::size_t SizeDerived<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. 它是否保证工作(即。getBufferBegin返回缓冲区的开头)

请随时参考标准以获取任何答案!

多谢

克里斯

<小时 />

替代

编辑:两者都等价,回答你更喜欢什么... 由于似乎有很多误解,没有人阅读解释注释:-/,让我在包含相同问题的替代版本中"改写"代码。分三个步骤:

  1. 调用实例getDataN<100>(static_cast<void*>(&d));getData4(static_cast<Base*>(&d));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. 调用getMemN<100>(static_cast<void*>(&d));/getMem5(static_cast<Data*>(&d));表示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. 琐碎
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;

仍然不清楚

下面的论证是,标准布局类的基类可指向派生类的指针互转换是不正确的。更准确地说,仅当派生类没有任何成员(包括基类的成员)时,它才成立。因此,使用C的奇怪讨论不起作用,因为C继承了Derived的成员并称他们为C的成员。

由于BaseDerived不是指针可相互转换的,因此使用std::launder来访问Derived的数据(见下文)是违反标准的,因为Derived的对象表示无法从指向Base实例的指针访问。因此,即使指向Base的指针与指向Derived的指针具有相同的值,通过Base::getHeader进行的访问也不一定是定义的行为 - 可能是未定义的行为,因为没有理由不这样认为。 注意:编译器不能假定此数据不是通过Base指针访问的,因为数据在static_castDerived后即可访问,因此无法对此数据应用优化。但是,如果您使用reinterpret_cast,它仍然是未定义的行为(即使指针的值相同)。

问题:标准中是否有任何内容强制要求指向Derived的指针也是指向Base的指针?它们明确可能具有相同的地址,但它们是否保证相同?(至少对于标准布局... 或者换句话说,reinterpret_cast<Base*>(&d)Derived d指向基子对象的明确定义的指针?(无论可访问性如何)

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

<小时 />

旧答案

是的,呈现的代码既定义明确,又按预期运行。让我们逐一看一下Base::getBufferBeginBase::getDataBase::getHeader的关键方法。

Base::getBufferBegin

首先,让我们展示一系列定义良好的强制转换,这些强制转换将使请求的强制转换从 this 指针到数组中第一个元素的指针dataDerived实例中。其次,证明给定的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<std::byte *>(b_p);

[basic.compound].4 + 4.3 表示(改写)指向没有成员的标准布局类实例的指针是指针互转换的,其地址与其任何基类相同。

因此,如果C是 Derived<100> 的标准布局子类,则指向C实例的指针将指向指向子对象Derived<100>的指针和指向子对象Base的指针的指针可相互转换。通过指针互转换性的传递性([basic.compound].4.4),指向Base的指针可指向指向Derived<100>的指针,如果存在这样的类。要么我们将C<Size>定义为这样的类并使用C<100>而不是Derived<Size>,要么只是接受从任何目标文件中都无法预测是否存在这样的类C,因此确保这一点的唯一方法是无论这样的类C(及其存在)如何,这两者都是指针可相互转换的。特别是,可以使用auto * d_p = reinterpret_cast<Derived<100> *>(b_p);代替static_cast缺少:可以使用auto * d_p = reinterpret_cast<Derived<100> *>(b_p);代替static_cast

最后一步Base;;getBuferBegin,我们可以用*reinterpret_cast<std::byte*>(b_p);替换以上所有内容吗?首先,是的,我们被允许进行这种转换,因为始终允许转换为二进制表示(通过 [basic.lval].11),并且不会更改指针²的值!其次,这个强制转换给出的结果与我们刚刚显示的相同的结果,上面的转换都可以替换为reinterpret_casts(不改变值²)。

总而言之,这表明Base::getBufferBegin()是明确定义的并且按预期运行(返回的指针指向子类的缓冲区数据中的第一个元素)。

Base::getHeader

Derived<Size>的构造函数在数组数据的第一个字节处构造一个标头实例。

通过上面的Base::getBufferBegin给出了一个指向这个字节的指针。问题仍然存在,是否允许我们通过此指针访问标头。为简单起见,让我在这里引用 cpp首选项(确保标准中相同(但不太容易理解)): 引用那里的"注释"

std::launder 的典型用途包括: [...]获取指向通过放置 new 创建的对象的指针,从指向为该对象提供存储的对象的指针。

这正是我们在这里所做的,所以一切都很好,不是吗?不,还没有。看看std::launder的要求,我们需要"每个可以通过结果到达的字节都可以通过p[给定的指针]到达"。但这里的情况是这样吗?

答案是肯定的,令人惊讶的是。通过上面的论证(只需搜索 [basic.compound].4.4 ;))给出指向Base的指针可相互转换为Derived<Size>。根据通过指针对字节的可访问性的定义,这意味着可以通过指向Base的指针访问Derived<Size>的完整二进制表示形式(请注意,这仅适用于标准布局类!因此,reinterpret_cast<Header*>(this);给出了一个指向Header实例的指针,通过该指针可以访问Header二进制表示的每个字节,从而满足std::launder的条件。因此,std::launder(是一个 noop)会产生一个指向标头的有效对象指针。

缺少:通过点到Base访问Derived的二进制表示(没有static_cast用法!

我们需要那个std::launder吗?是的,我们正式这样做,因为这个reinterpret_cast在非指针可相互转换的对象之间包含两个强制转换,即 (1) 指向数组的指针和指向其第一个元素的指针(这似乎是完整讨论中最微不足道的一个)和 (2) 指向Header二进制表示的指针和指向对象Header的指针,标头是标准布局的事实不会改变任何东西!

Base::getData

请参阅Base::getHeader的唯一补充,即我们允许进行给定的指针算术(对于0<=idxidx<=Size),因为给定的指针指向数组data的第一个元素,并且可以通过指针访问完整的数组data(见上文)。

做。

你为什么需要这个讨论

编译器的认证确保我们可以依靠它做标准所说的(仅此而已)。通过上述,标准说我们可以做这些事情。

为什么需要这种结构

获取对非平凡容器(例如列表,映射)的引用到静态内存缓冲区,而无需

  1. 包含类型中容器的大小 - 否则,我们有相当多的模板 - ,
  2. 未定义行为,
  3. 通过使用存储指向标头和数据的指针的(可能是临时的)API 类间接使用存储,因为这会将问题转移到容器强制转换的用户,
  4. 容器类的用户应用的用户定义的强制转换,因为它们会向我们的基础库的用户发出警告,
  5. 保持标准布局(是的,仅当缓冲区中存储的Header和类型也是标准布局时,情况才会如此),
  6. 不使用指针。

后两者是必需的,因为结构是通过IPC发送的。


²:是,将一个指针类型reinterpret_cast到另一个指针类型不会更改该值。每个人都假设这一点,但它也在标准中([expr.static.cast].13):

Blockquote 如果原始指针值表示内存中某个字节的地址 A,而 A 不满足 T 的对齐要求,则未指定生成的指针值。(...)否则,指针值在转换时保持不变。

这表明static_cast<T*>(static_cast<void*>(u))不会更改指针,并且通过 [expr.reinterpret.cast].7,这相当于相应的reinterpret_cast

相关内容

  • 没有找到相关文章

最新更新