Неопределенное поведение в примере std::uninitialized_default_construct на cppreference?

#c #language-lawyer #pointer-arithmetic #placement-new

Вопрос:

В их примере из 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.
 

У меня есть три вопроса. (Отметьте как ********** )

  1. Поскольку S и unsigned char не являются взаимозаменяемыми указателями, будет last указывать на конец прошлого mem ? Является ли арифметика указателей здесь неопределенным поведением?
  2. Требуется ли здесь std::отмывание, чтобы сделать first и last на самом деле указать S ? например first = std::launder(first); last = first n;
  3. Действительно ли это неопределенное поведение для печати элементов v ? CWG 1997 говорит о unsigned char[] хранении, но приведенный выше пример инициализирован v с int помощью s.

Ответ №1:

  1. Это unsigned char еще один способ сказать 1 byte , поэтому распределение выполняется для размера struct S (в байтах), умноженного на количество элементов n . Этого mem типа unsigned char [] , поэтому, согласно [усл.массив]/1 это будет эквивалент unsigned char * , который указывает на первый элемент в массиве, и как [выражение.переиначить.литые]/4 В reinterpret_cast будет преобразовать его в struct S* что вызовет first , которая объявлена как auto быть создан в качестве struct S* , поэтому заявление является взаимозаменяемым с C форма:
     struct S* first = (struct S*) amp;mem
     

    Поскольку параметр first разрешен как struct S* , следовательно last , он также будет разрешен для того же типа, и арифметика указателей будет работать нормально.

  2. В этом нет необходимости std::launder , так как оператор auto first {reinterpret_cast<S*>(mem)}; уже инициализировал указатель на первый элемент mem , а следующий оператор с арифметикой указателя уже установил значение last на 3-й элемент.
  3. Я думаю, что это правильно, что это неопределенное поведение по состоянию на CWG 1997. Тот int [] , который является типом v , не имеет конструктора по умолчанию, поэтому v он не может быть инициализирован. Согласно определению, значения останутся неопределенными. Запутанная часть заключается в том , что элементы v уже инициализированы компилятором в 1, 2, 3, 4 , поэтому ожидается, что эти значения будут сохранены (поскольку они уже находятся в ячейке памяти), однако со стандартной точки зрения нет определения, будут ли v эти значения сохраняться после std::uninitialized_default_construct(std::begin(v), std::end(v)) .

Комментарии:

1. » reinterpret_cast Вызовет первое, которое объявлено как auto созданное как struct S* » к сожалению, S имеет нетривиальный деструктор, не позволяющий ему быть классом с неявным временем жизни для [class.prop]/9 .

2. @L. F. даже если S бы был ILT, reinterpret_cast не создавал бы указатель на объект типа S из указателя на unsigned char

3. @L. F. согласно [class.dtor]/2 , в случае, если предполагаемый деструктор опущен, то же самое будет неявно объявлено.

4. @jordanvrtanoski Да, но неявно объявленный деструктор нетривиален, потому что деструктор string is.

5. @L. F. Я вижу смысл, но я не понимаю, как это связано с std::uninitialized_default_construct() примером. Приведение в примере работает, потому что оба значения (значение lvalue и значение rvalue) интерпретируются как указатели.