c++ 使用placement-new、复制存储然后访问值是否为未定义行为?

dwbf0jvd  于 2023-03-05  发布在  其他
关注(0)|答案(2)|浏览(103)

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列出的那些要求的任何可调用对象一起使用。

vm0i2vca

vm0i2vca1#

template<class T>
T* laundry_pod(void* ptr){
  char buff[sizeof(T)];
  std::memcpy(buff, ptr, sizeof(T));
  auto* r=::new(ptr)T;
  std::memcpy(ptr, buff, sizeof(T));
  return r;
}

这可能是有用的。2它将编译器对一个位置的数据的解释从一种pod类型转换为另一种pod类型,并编译为优化下的noop。
因此,将您的缓冲区“清洗”pod到T,向其写入,“清洗”pod回到通用存储。要读取,“清洗”pod到T。注意,通过T指针的所有写入之后都应将其清洗回通用存储,以避免使用别名规则优化掉看似已丢弃的指针上的写入。

gj3fmq9x

gj3fmq9x2#

这里有三个步骤:在char数组中创建T对象、复制该数组以及访问T。我们将依次讨论它们。
在对齐的缓冲区中显式创建对象只能使用unsigned charstd::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开始工作:
开始charunsigned charstd​::​byte数组的生存期的操作将在数组占用的存储区域内隐式创建对象。
[* 注5:* 数组对象为这些对象提供存储。-end note]
...
(我相信这里包含的char是CWG的问题,它最终会被删除,但我现在懒得仔细检查。)
S被复制构造,并且新对象的生存期开始时,unsigned char数组成员的生存期也开始。如果需要,这将在新数组中隐式创建一个T对象。当字节实际上从旧的S对象复制到新对象时,则应用[basic.types.general]/3:
对于普通可复制类型T的两个不同对象obj1obj2,其中obj1obj2都不是潜在重叠的子对象,如果组成obj1的底层字节([intro.memory])被复制到obj2,则31个obj2随后将保持与obj1相同的值。
(The脚注指出std::memcpystd::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对象时,行为将是未定义的。

相关问题