C ++ reinterpret_cast безопасность с ссылками на массивы и назначением перемещения / копирования - PullRequest
0 голосов
/ 27 февраля 2019

Мои товарищи по команде пишут реализацию фиксированного размера std::vector для критически важного приложения.Нам не разрешено использовать выделение кучи, поэтому они создали простую оболочку массива, например:

template <typename T, size_t NUM_ITEMS>
class Vector
{
public:
    void push_back(const T& val);

    ...more vector methods

private:
    // Internal storage
    T storage_[NUM_ITEMS];

    ...implementation
};

Проблема, с которой мы столкнулись в этой реализации, заключается в том, что для нее требуются элементы, представляющие конструкторы по умолчанию (что не является обязательным требованием).std::vector и создали трудности с портированием).Я решил взломать их реализацию, чтобы она вела себя как std::vector, и придумал следующее:

template <typename T, size_t NUM_ITEMS>
class Vector
{
public:
    void push_back(const T& val);

    ...more vector methods
private:
    // Internal storage
    typedef T StorageType[NUM_ITEMS];
    alignas(T) char storage_[NUM_ITEMS * sizeof(T)];

    // Get correctly typed array reference
    StorageType& get_storage() { return reinterpret_cast<T(&)[NUM_ITEMS]>(storage_); }
    const StorageType& get_storage() const { return reinterpret_cast<const T(&)[NUM_ITEMS]>(storage_); }
};

Затем я смог просто найти и заменить storage_ на get_storage(), и все заработало,Пример реализации push_back может тогда выглядеть следующим образом:

template <typename T, size_t NUM_ITEMS>
void Vector<T, NUM_ITEMS>::push_back(const T& val)
{
    get_storage()[size_++] = val;
}

На самом деле, это сработало так легко, что заставило меня задуматься ... Это хорошее / безопасное использование reinterpret_cast?Является ли код непосредственно над подходящей альтернативой размещению новым или есть риски, связанные с назначением копирования / перемещения неинициализированному объекту?

РЕДАКТИРОВАТЬ: В ответ на комментарий NathanOliver я должен добавить, что мы не можем использоватьSTL, потому что мы не можем скомпилировать его для нашей целевой среды и не можем его сертифицировать.

Ответы [ 2 ]

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

Код, который вы показали, безопасен только для типов POD (Plain Old Data), где представление объекта тривиально и, таким образом, присвоение неструктурированному объекту в порядке.

Если вы хотите, чтобы это работало ввсе общности (что я предполагаю, что вы делаете из-за использования шаблона), то для типа T это неопределенное поведение, чтобы использовать объект до его построения.То есть вы должны создать объект до того, как, например, вы назначите это место.Это означает, что вам нужно явно вызывать конструктор по требованию.Следующий блок кода демонстрирует пример этого:

template <typename T, size_t NUM_ITEMS>
void Vector<T, NUM_ITEMS>::push_back(const T& val)
{
    // potentially an overflow test here

    // explicitly call copy constructor to create the new object in the buffer
    new (reinterpret_cast<T*>(storage_) + size_) T(val);

    // in case that throws, only inc the size after that succeeds
    ++size_;
}

Приведенный выше пример демонстрирует размещение new, которое принимает форму new (void*) T(args...).Он вызывает конструктор, но фактически не выполняет выделение.Визуальным отличием является включение аргумента void* для самого оператора new, который является адресом объекта, на который нужно воздействовать и для которого вызывается конструктор.

И, конечно, при удалении элемента вы будетенужно уничтожить это явно.Чтобы сделать это для типа T, просто вызовите псевдо-метод ~T() для объекта.В контекстном контексте компилятор определит, что это означает, либо фактический вызов деструктора, либо отсутствие операций, например, для int или double.Это продемонстрировано ниже:

template<typename T, size_t NUM_ITEMS>
void Vector<T, NUM_ITEMS>::pop_back()
{
    if (size_ > 0) // safety test, you might rather this throw, idk
    {
        // explicitly destroy the last item and dec count
        // canonically, destructors should never throw (very bad)
        reinterpret_cast<T*>(storage_)[--size_].~T();
    }
}

Кроме того, я бы не стал возвращать ссылку на массив в вашем методе get_storage(), поскольку он имеет информацию о длине и, по-видимому, подразумевает, что все элементы действительны (созданы)объекты, которые, конечно, они не.Я предлагаю вам предоставить методы для получения указателя на начало непрерывного массива построенных объектов и еще один метод для получения количества построенных объектов.Это методы .data() и .size(), например std::vector<T>, которые сделают ваш класс менее раздражающим для опытных пользователей C ++.

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

Это хорошее / безопасное использование reinterpret_cast?

Является ли код непосредственно над подходящей альтернативой размещению новых

Нет.Нет.

или есть риски, связанные с назначением копирования / перемещения неинициализированному объекту?

Да.Поведение не определено.

  1. При условии, что память не инициализирована, копирование вектора имеет неопределенное поведение.
  2. Ни один объект типа T не запустил свое время жизни в ячейке памяти.Это очень плохо, когда T не является тривиальным.
  3. Реинтерпретация нарушает строгие правила псевдонимов.

Первое исправляется путем инициализации значения хранилища.Или сделав вектор не копируемым и неподвижным.

Секунда фиксируется с помощью размещения new.

Третья технически фиксируется с помощью указателя, возвращаемого позицией new, но вы можетеизбегайте сохранения этого указателя с помощью std::launder ing после переинтерпретации хранилища.

...