cppreference 上 std::uninitialized_default_construct 示例中的未定义行为?

问题描述

在他们的 std::uninitialized_default_constructexample 中:

    struct S { std::string m{ "Default value" }; };
 
    constexpr int n {3};
    alignas(alignof(S)) unsigned char mem[n * sizeof(S)];
 
    try
    {
        auto first {reinterpret_cast<S*>(mem)};
        auto last {first + n};  // (1)**********
 
        std::uninitialized_default_construct(first,last);

        // (2)**********
        for (auto it {first}; it != last; ++it) {
            std::cout << it->m << '\n';
        }
 
        std::destroy(first,last);
    }
    catch(...)
    {
        std::cout << "Exception!\n";
    }
 
    // Notice that for "trivial types" the uninitialized_default_construct
    // generally does not zero-fill the given uninitialized memory area.
    int v[] { 1,2,3,4 };
    const int original[] { 1,4 };
    std::uninitialized_default_construct(std::begin(v),std::end(v));

    // (3)**********
    // for (const int i : v) { std::cout << i << ' '; }
    // Maybe undefined behavior,pending CWG 1997.

我有三个问题。 标记为**********)

  1. 既然 Sunsigned char 不是指针可互转换,那么 last 会指向 mem 的结尾吗?此处是否存在指针算术未定义行为?
  2. 是否需要 std::launder 来使 firstlast 实际上指向 S?例如first = std::launder(first); last = first + n;
  3. 打印 v 的元素真的是未定义的行为吗? CWG 1997 谈论 unsigned char[] 存储,但上面的示例已使用 v 初始化 int

解决方法

  1. unsigned char1 byte 的另一种说法,因此分配是按照 struct S 的大小(以字节为单位)乘以元素数 n . mem 的类型为 unsigned char [],因此,根据 [conv.array]/1,它将相当于指向数组中第一个元素的 unsigned char *,并且根据 {{3} }} reinterpret_cast 会将其转换为 struct S*,这将导致声明为 firstauto 被创建为 struct S*,因此语句可与 C 形式互换:
    struct S* first = (struct S*) &mem
    
    由于 first 被解析为 struct S*,因此 last 也将被解析为相同的类型并且指针算法将正常工作。
  2. 不需要 std::launder,因为语句 auto first {reinterpret_cast<S*>(mem)}; 已经初始化了指向 mem 中第一个元素的指针,并且带有指针算法的下一条语句已经设置了 { {1}} 到第三个元素。
  3. 我认为从 [expr.reinterpret.cast]/4 开始这是未定义的行为是正确的。 lastint [] 的类型,没有默认构造函数,因此无法初始化 v。根据定义,这些值将保持未确定状态。令人困惑的部分是 v 的元素已经被编译器初始化为 v,因此期望这些值将被保留(因为它们已经在内存位置中),但是从标准从角度来看,无法确定 1,2,3,4 是否会在 v 之后保持此值。