Хорошо ли интерпретируется указатель на первый член как сам класс? - PullRequest
15 голосов
/ 02 мая 2019

У меня есть код, который выглядит следующим образом:

template<typename T>
struct memory_block {
    // Very not copiable, this class cannot move
    memory_block(memory_block const&) = delete;
    memory_block(memory_block const&&) = delete;
    memory_block(memory_block&) = delete;
    memory_block(memory_block&&) = delete;
    memory_block& operator=(memory_block const&) = delete;
    memory_block& operator=(memory_block&&) = delete;

    // The only constructor construct the `data` member with args
    template<typename... Args>
    explicit memory_block(Args&&... args) noexcept :
        data{std::forward<Args>(args)...} {}

    T data;
};

template<typename T>
struct special_block : memory_block<T> {
    using memory_block<T>::memory_block;
    std::vector<double> special_data;
};

// There is no other inheritance. The hierarchy ends here.

Теперь я должен сохранить эти типы в стертом хранилище. Я выбрал вектор void* в качестве контейнера. Я вставляю указатели члена data в вектор:

struct NonTrivial { virtual ~NonTrivial() {} };

// exposed to other code
std::vector<void*> vec;

// My code use dynamic memory instead of static
// Data, but it's simpler to show it that way.
static memory_block<int> data0;
static special_block<NonTrivial> data1;

void add_stuff_into_vec() {
    // Add pointer to `data` member to the vector.
    vec.emplace_back(&(data0->data));
    vec.emplace_back(&(data1->data));
}

Затем в коде я получаю доступ к данным:

// Yay everything is fine, I cast the void* to it original type
int* data1 = static_cast<int*>(vec[0]);
NonTrivial* data1 = static_cast<NonTrivial*>(vec[1]);

Проблема в том, что я хочу получить доступ к special_data в нетривиальном случае:

// Pretty sure this cast is valid! (famous last words)
std::vector<double>* special = static_cast<special_block<NonTrivial>*>(
    static_cast<memory_block<NonTrivial>*>(vec[1]) // (1)
);

Итак, вопрос

Проблема возникает в строке (1): у меня есть указатель на data (типа NonTrivial), который является членом memory_block<NonTrivial>. Я знаю, что void* всегда будет указывать на первый элемент данных memory_block<T>.

Значит, приведение void* к первому члену класса в сейф класса? Если нет, есть ли другой способ сделать это? Если это может сделать все проще, я могу избавиться от наследства.

Кроме того, у меня нет проблем с использованием std::aligned_storage в этом случае. Если это решит проблему, я воспользуюсь этим.

Я надеялся, что стандартная раскладка поможет мне в этом случае, но мое статическое утверждение, похоже, не работает.

Мое статическое утверждение:

static_assert(
    std::is_standard_layout<special_block<NonTrivial>>::value,
    "Not standard layout don't assume anything about the layout"
);

1 Ответ

15 голосов
/ 02 мая 2019

Пока memory_block<T> является типом стандартной компоновки [class.prop] / 3 , адрес memory_block<T> и адрес его первого члена data являются взаимозаменяемыми по указателю [basic.compound] /4.3.Если это так, стандарт гарантирует, что вы можете reinterpret_cast получить указатель на один из указателя на другой.Как только у вас нет стандартного макета, такой гарантии нет.

В вашем конкретном случае memory_block<T> будет стандартным макетом, пока T является стандартным макетом.Ваш special_block никогда не будет стандартным макетом, потому что он содержит std::vector (как также указал @NathanOliver в своем комментарии ниже), который не обязательно является стандартным макетом.В вашем случае, поскольку вы просто вставляете указатель на член data субобъекта memory_block<T> вашего special_block<T>, вы все равно можете сделать это, если T стандартного макета, если вы reinterpret_cast вашvoid* обратно на memory_block<T>*, а затем static_cast на special_block<T>* (при условии, что вы точно знаете, что динамический тип всего объекта на самом деле special_block<T>).К сожалению, как только NonTrivial входит в картину, все ставки отключены, потому что NonTrivial имеет виртуальный метод и, следовательно, не является стандартной раскладкой, что также означает, что memory_block<NonTrivial> не будет стандартной раскладкой…

Одна вещь, которую вы могли бы сделать, это, например, иметь только буфер для хранения T в вашем memory_block и затем создать фактический T внутри хранилища data с помощью размещения new.например:

#include <utility>
#include <new>

template <typename T>
struct memory_block
{
    alignas(T) char data[sizeof(T)];

    template <typename... Args>
    explicit memory_block(Args&&... args) noexcept(noexcept(new (data) T(std::forward<Args>(args)...)))
    {
        new (data) T(std::forward<Args>(args)...);
    }

    ~memory_block()
    {
        std::launder(reinterpret_cast<T*>(data))->~T();
    }

    …
};

Таким образом memory_block<T> всегда будет стандартным макетом…

...