设S
是一个结构体类型,它包含一个具有最大对齐和固定大小的字符数组data
,其思想是S
能够存储T
类型的任何对象,其大小不超过限制,并且是可平凡复制构造和可平凡析构的。
static constexpr std::size_t MaxSize = 16;
struct S {
alignas(alignof(std::max_align_t)) char data[MaxSize];
};
Placement-new用于将T
类型的对象构造为新S
对象的字符数组,然后该对象被复制任意次,包括返回和传值。
template <typename T>
S wrap(T t) {
static_assert(sizeof(T) <= MaxSize, "");
static_assert(std::is_trivially_copy_constructible_v<T>, "");
static_assert(std::is_trivially_destructible_v<T>, "");
S s;
new(reinterpret_cast<T *>(s.data)) T(t);
return s;
}
稍后给定这个S
值的副本,reinterpret_cast
被用来从指向字符数组开始的指针获得T*
,然后T
对象以某种方式被访问,T
类型与创建该值时相同。
void access(S s) {
T *t = reinterpret_cast<T *>(s.data);
t->print();
}
我想知道这个方案是否涉及未定义的行为,如何解决,例如我担心:
- “重用对象存储”是否存在问题,即
std::launder
设计要解决的问题?我不确定在那里构造了T
的示例后,将data
作为字符数组访问是否有效,在访问值的地方是否需要std::launder
,为什么? S
生成的复制构造函数复制了data
中的所有字节,是否因为某些字节可能尚未初始化而出现问题?我担心sizeof(T)
以外的字节以及T
对象中可能未初始化的字节(例如填充)。
我的用例是实现一个非常轻量级的多态函数 Package 器,它能够与满足我为T
列出的那些要求的任何可调用对象一起使用。
2条答案
按热度按时间vm0i2vca1#
这可能是有用的。2它将编译器对一个位置的数据的解释从一种pod类型转换为另一种pod类型,并编译为优化下的noop。
因此,将您的缓冲区“清洗”pod到T,向其写入,“清洗”pod回到通用存储。要读取,“清洗”pod到T。注意,通过T指针的所有写入之后都应将其清洗回通用存储,以避免使用别名规则优化掉看似已丢弃的指针上的写入。
gj3fmq9x2#
这里有三个步骤:在
char
数组中创建T
对象、复制该数组以及访问T
。我们将依次讨论它们。在对齐的缓冲区中显式创建对象只能使用
unsigned char
和std::byte
缓冲区。这些缓冲区在语言中有明确的规则,允许它们为在其中创建的对象“提供存储”。参见[intro.object]/3和其中的示例。当您使用char
缓冲区时,它不为T
对象提供存储。并且T
对象没有“嵌套在”缓冲区中。在[basic.life]/1.5下,T
对象的创建结束了T
对象占用其存储的char
对象的生存期(尽管本节没有明确说明封闭数组对象的生存期是否也结束了)。假设char
缓冲器将用于答案的剩余部分。现在我们来看看嵌套了
T
对象的unsigned char
缓冲区是否可以复制为unsigned char
缓冲区而不会导致UB。通过copy construction,[intro.object]/13开始工作:开始
char
、unsigned char
或std::byte
数组的生存期的操作将在数组占用的存储区域内隐式创建对象。[* 注5:* 数组对象为这些对象提供存储。-end note]
...
(我相信这里包含的
char
是CWG的问题,它最终会被删除,但我现在懒得仔细检查。)当
S
被复制构造,并且新对象的生存期开始时,unsigned char
数组成员的生存期也开始。如果需要,这将在新数组中隐式创建一个T
对象。当字节实际上从旧的S
对象复制到新对象时,则应用[basic.types.general]/3:对于普通可复制类型
T
的两个不同对象obj1
和obj2
,其中obj1
和obj2
都不是潜在重叠的子对象,如果组成obj1
的底层字节([intro.memory])被复制到obj2
,则31个obj2
随后将保持与obj1
相同的值。(The脚注指出
std::memcpy
和std::memmove
是实现这一点的方法,但是S
的复制构造函数也可以实现。)S
的复制赋值可以很好地工作,只要你不期望它执行隐式对象创建。如果LHS已经有一个T
对象嵌套在它里面,并且RHS也有,那么[basic.types.general]/13意味着赋值之后,LHS的T
对象将保持与RHS的T
对象相同的值。最后,我们来看看访问的问题,在这个例子中,我们从指向
unsigned char
数组的指针和指向reinterpret_cast
数组的指针开始,这实际上并没有达到预期的效果。因为unsigned char
数组不能与它为其提供存储的T
对象 * 指针互换 *。请参阅[expr.reinterpret.cast]/7和[expr. static.cast]/14,这意味着当不满足指针可相互转换性准则时,reinterpret_cast
的结果是指向原始对象的指针(不是指向您试图访问的T
对象的指针)。必须使用函数std::launder
将从reinterpret_cast
返回的T*
值转换为指向T
的实际有效指针如果不这样做,当你试图通过一个实际上并不指向T
对象的指针访问一个T
对象时,行为将是未定义的。