Undefined behavior in std::uninitialized_default_construct example on cppreference?

Viewed 173

In their example of std::uninitialized_default_construct:

    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, 2, 3, 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.

I have three questions. (Mark as ********** )

  1. Since S and unsigned char aren't pointer-interconvertible, will last point to past-the-end of mem? Is pointer arithmetic undefined behavior here?
  2. Is std::launder needed here to make first and last actually point to S? e.g. first = std::launder(first); last = first + n;
  3. Is it really undefined behavior to print elements of v? CWG 1997 talk about unsigned char[] storage, but the above example has initialized v with ints.
1 Answers
  1. The unsigned char is another way of saying 1 byte, so the allocation is performed for the size of the struct S (in bytes) multiplied by the number of elements n. The mem is of type unsigned char [], therefore, as per [conv.array]/1 it will be equivalent of unsigned char * which points to the first element in the array, and as per [expr.reinterpret.cast]/4 the reinterpret_cast will convert it to struct S* which will cause the first, which is declared as auto to be created as a struct S*, so the statement is interchangeable with the C form:
    struct S* first = (struct S*) &mem
    
    Since the first is resolved as struct S*, therefore the last will also be resolved to the same type and the pointer arithmetic will work fine.
  2. There is no need for the std::launder since the statement auto first {reinterpret_cast<S*>(mem)}; already had initialized the pointer to the first element in mem, and the next statement with the pointer arithmetic already set the last to the 3rd element.
  3. I think this is correct that this is undefined behavior as of CWG 1997. The int [], which is the type of v, has no default constructor, therefore the v can not be initialized. As per definition, the values will remain undetermined. The confusing part is that elements of v are already initialized by the compiler to the 1, 2, 3, 4, so expectation is that this values will be preserved (as they are already in the memory location), however from the standard perspective there is no determination if v will maintain this values after the std::uninitialized_default_construct(std::begin(v), std::end(v)).
Related