首先提前一个词:下面的代码不应该按原样使用,只是工作代码到临界点的浓缩。问题只是以下代码在哪里违反了标准(C++17,但C++20也可以),如果没有,标准是否保证"正确的输出"?这不是初学者如何编写代码或类似内容的示例,这纯粹是关于标准的问题。(通过下午要求:替代版本在下面进一步)
假设以下类Base
永远不会直接实例化,而只是通过某些std::size_t Size
的Derived<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
等,或者在概括时要注意什么(填充,对齐等),而只是
- 违反标准的地方,如果没有
- 它是否保证工作(即。
getBufferBegin
返回缓冲区的开头)
请随时参考标准以获取任何答案!
多谢
克里斯
<小时 />替代
编辑:两者都等价,回答你更喜欢什么... 由于似乎有很多误解,没有人阅读解释注释:-/,让我在包含相同问题的替代版本中"改写"代码。分三个步骤:
- 调用实例
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); }
- 调用
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); }
- 琐碎
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
的成员。
由于Base
和Derived
不是指针可相互转换的,因此使用std::launder
来访问Derived
的数据(见下文)是违反标准的,因为Derived
的对象表示无法从指向Base
实例的指针访问。因此,即使指向Base
的指针与指向Derived
的指针具有相同的值,通过Base::getHeader
进行的访问也不一定是定义的行为 - 可能是未定义的行为,因为没有理由不这样认为。 注意:编译器不能假定此数据不是通过Base
指针访问的,因为数据在static_cast
Derived
后即可访问,因此无法对此数据应用优化。但是,如果您使用reinterpret_cast
,它仍然是未定义的行为(即使指针的值相同)。
问题:标准中是否有任何内容强制要求指向Derived
的指针也是指向Base
的指针?它们明确可能具有相同的地址,但它们是否保证相同?(至少对于标准布局... 或者换句话说,reinterpret_cast<Base*>(&d)
是Derived d
指向基子对象的明确定义的指针?(无论可访问性如何)
PS:对于 C++20,我们有std::is_pointer_interconvertible_base_of
可以检查它是否适用于给定类型。
旧答案
是的,呈现的代码既定义明确,又按预期运行。让我们逐一看一下Base::getBufferBegin
、Base::getData
和Base::getHeader
的关键方法。
Base::getBufferBegin
首先,让我们展示一系列定义良好的强制转换,这些强制转换将使请求的强制转换从 this 指针到数组中第一个元素的指针data
Derived
实例中。其次,证明给定的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_cast
s(不改变值²)。
总而言之,这表明Base::getBufferBegin()
是明确定义的并且按预期运行(返回的指针指向子类的缓冲区数据中的第一个元素)。
Base::getHeader
Derived<Size>
的构造函数在数组数据的第一个字节处构造一个标头实例。
Base::getBufferBegin
给出了一个指向这个字节的指针。std::launder 的典型用途包括: [...]获取指向通过放置 new 创建的对象的指针,从指向为该对象提供存储的对象的指针。
这正是我们在这里所做的,所以一切都很好,不是吗?不,还没有。看看std::launder
的要求,我们需要"每个可以通过结果到达的字节都可以通过p[给定的指针]到达"。但这里的情况是这样吗?
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<=idx
和idx<=Size
),因为给定的指针指向数组data
的第一个元素,并且可以通过指针访问完整的数组data
(见上文)。
做。
你为什么需要这个讨论
编译器的认证确保我们可以依靠它做标准所说的(仅此而已)。通过上述,标准说我们可以做这些事情。
为什么需要这种结构
获取对非平凡容器(例如列表,映射)的引用到静态内存缓冲区,而无需
- 包含类型中容器的大小 - 否则,我们有相当多的模板 - ,
- 未定义行为,
- 通过使用存储指向标头和数据的指针的(可能是临时的)API 类间接使用存储,因为这会将问题转移到容器强制转换的用户,
- 容器类的用户应用的用户定义的强制转换,因为它们会向我们的基础库的用户发出警告,
- 保持标准布局(是的,仅当缓冲区中存储的
Header
和类型也是标准布局时,情况才会如此), - 不使用指针。
后两者是必需的,因为结构是通过IPC发送的。
²:是,将一个指针类型reinterpret_cast
到另一个指针类型不会更改该值。每个人都假设这一点,但它也在标准中([expr.static.cast].13):
Blockquote 如果原始指针值表示内存中某个字节的地址 A,而 A 不满足 T 的对齐要求,则未指定生成的指针值。(...)否则,指针值在转换时保持不变。
这表明static_cast<T*>(static_cast<void*>(u))
不会更改指针,并且通过 [expr.reinterpret.cast].7,这相当于相应的reinterpret_cast