О выровненном хранилище и тривиально копируемых / уничтожаемых типах - PullRequest
0 голосов
/ 04 февраля 2019

У меня была интересная дискуссия с парнем, умнее меня, и у меня остался открытый вопрос о выровненных хранилищах и тривиально копируемых / разрушаемых типах.

Рассмотрим следующий пример:

#include <type_traits>
#include <vector>
#include <cassert>

struct type {
    using storage_type = std::aligned_storage_t<sizeof(void *), alignof(void *)>;
    using fn_type = int(storage_type &);

    template<typename T>
    static int proto(storage_type &storage) {
        static_assert(std::is_trivially_copyable_v<T>);
        static_assert(std::is_trivially_destructible_v<T>);
        return *reinterpret_cast<T *>(&storage);
    }

    std::aligned_storage_t<sizeof(void *), alignof(void *)> storage;
    fn_type *fn;
    bool weak;
};

int main() {
    static_assert(std::is_trivially_copyable_v<type>);
    static_assert(std::is_trivially_destructible_v<type>);

    std::vector<type> vec;

    type t1;
    new (&t1.storage) char{'c'};
    t1.fn = &type::proto<char>;
    t1.weak = true;
    vec.push_back(t1);

    type t2;
    new (&t2.storage) int{42};
    t2.fn = &type::proto<int>;
    t2.weak = false;
    vec.push_back(t2);

    vec.erase(std::remove_if(vec.begin(), vec.end(), [](const auto &t) { return t.weak; }), vec.end());

    assert(vec.size() == 1);
    assert(!vec[0].weak);
    assert(vec[0].fn(vec[0].storage) == 42);
}

Это упрощенная версия кейса.Я действительно надеюсь, что не делал ошибок и не слишком упрощал.

Как видите, идея в том, что существует тип с именем type (называть вещи сложно, вы знаете), имеющий три данныхчлены:

  • storage, то есть набор байтов размером sizeof(void *)
  • fn указатель на функцию типа int(storage_type &)
  • weak бесполезный бул, используемый только для ознакомления с примером.

. Для создания новых экземпляров type (см. Функцию main) я помещаю значение (либо int, либоchar) в области хранения и правильной специализации шаблона статической функции proto в fn.
Позже, когда я хочу , вызовите fn и получите целочисленное значениеон возвращается, я делаю что-то вроде этого:

int value = type_instance.fn(type_instance.storage);

Пока все хорошо.Несмотря на то, что это рискованно и подвержено ошибкам (но это пример, реального варианта использования нет), это работает .
Обратите внимание, что type и все типы, которые я вставил вхранилище (int и char в примере) должно быть как тривиально копируемым, так и тривиально разрушаемым.Это также является основой обсуждения, которое у меня было.

Проблема (или лучше сомнение ) возникает, когда я помещаю экземпляры типов, например, в вектор (см. функцию main) и решите удалить один из них из массива, чтобы некоторые другие перемещались, чтобы сохранить его упакованным.
В общем, я больше не уверен в том, что происходиткогда я хочу скопировать или переместить экземпляры type и, если это UB или нет.

Я предположил, что было разрешено быть типами, помещаемыми в хранилище, тривиально копируемыми и тривиально разрушаемыми.С другой стороны, мне сказали, что это прямо не разрешено стандартом, и это можно считать доброкачественным UB , потому что практически все компиляторы фактически позволяют вам это делать (я могугарантируя это, кажется, что работа везде для некоторых определений работа ).

Итак, вопрос: это разрешено или UB и что я могу сделать, чтобыобойти проблему во втором случае?Кроме того, собирается ли C ++ 20 что-то изменить для этого?

1 Ответ

0 голосов
/ 04 февраля 2019

Эта проблема сводится к тому, что LanguageLawyer предложил:

alignas(int) unsigned char buff1[sizeof(int)];
alignas(int) unsigned char buff2[sizeof(int)];

new (buff1) int {42};
std::memcpy(buff2, buff1, sizeof(buff1));

assert(*std::launder(reinterpret_cast<int*>(buff2)) == 42); // is it ok?

Другими словами - когда я копирую байты вокруг, я также копирую вокруг объекта?-ness "?buff1, безусловно, предоставляет хранилище для int - когда мы копируем эти байты, buff2 также теперь предоставляет хранилище для int?

И ответ ... нет.Существует ровно четыре способа создания объекта для каждого [intro.object]:

Объект создается по определению с помощью нового выражения ([expr.new]), когда неявно изменяется активный член объединения или когда создается временный объект ([conv.rval], [class.teven]).

Ничего из этого не произошло здесь,поэтому у нас нет объектов в buff2 любого вида (кроме обычного массива unsigned char), поэтому поведение не определено.Проще говоря, memcpy не создает объекты.

В исходном примере неявное создание объекта требует только 3-я строка:

assert(vec.size() == 1); // ok
assert(!vec[0].weak);    // ok
assert(vec[0].fn(vec[0].storage) == 42); // UB

Вот почему P0593 существует и имеет специальный раздел для memmove / memcpy:

Вызов memmove ведет себя так, как если бы он

  • копирует исходное хранилище во временную область
  • неявно создает объекты в целевом хранилище, а затем
  • копирует временное хранилище в целевое хранилище.

Это позволяет memmove сохранять типы тривиально копируемых объектов или использоваться для переинтерпретации байтового представления одного объекта как объекта другого объекта.

Это то, что вам нужно здесь - этошаг неявного создания объекта в настоящее время отсутствует в C ++.

Тем не менее, вы можете более или менее полагаться на это "правильное поступление", учитывая, что просто огромный массив кода C ++, существующий сегодня, полагается на этот код для "просто работы".

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...