是否将指向第一个成员的指针解释为类本身定义正确?

Gui*_*cot 15 c++ casting void-pointers c++11

我有一些看起来像这样的代码:

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.
Run Code Online (Sandbox Code Playgroud)

现在,我必须将这些类型存储到类型已擦除的存储中。我选择一个向量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));
}
Run Code Online (Sandbox Code Playgroud)

然后在代码后面,我访问数据:

// 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]);
Run Code Online (Sandbox Code Playgroud)

问题是我想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)
);
Run Code Online (Sandbox Code Playgroud)

所以现在,问题

问题出现在第1行(1):我有一个指向data(类型的NonTrivial)指针,它是的成员memory_block<NonTrivial>。我知道void*总是会指向的第一个数据成员memory_block<T>

那么void*将class的第一个成员强制转换为class是安全的吗?如果没有,还有另一种方法吗?如果可以简化事情,我可以摆脱继承。

另外,std::aligned_storage在这种情况下使用我也没有问题。如果那可以解决问题,我将使用它。

我希望标准布局可以在这种情况下为我提供帮助,但是我的静态断言似乎失败了。

我的静态断言:

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

Mic*_*zel 15

As long as memory_block<T> is a standard-layout type [class.prop]/3, the address of a memory_block<T> and the address of its first member data are pointer interconvertible [basic.compound]/4.3. If this is the case, the standard guarantees that you can reinterpret_cast to get a pointer to one from a pointer to the other. As soon as you don't have a standard-layout type, there is no such guarantee.

For your particular case, memory_block<T> will be standard-layout as long as T is standard-layout. Your special_block will never be standard layout because it contains an std::vector (as also pointed out by @NathanOliver in his comment below), which is not guaranteed to be standard layout. In your case, since you just insert a pointer to the data member of the memory_block<T> subobject of your special_block<T>, you could still make that work as long as T is standard-layout if you reinterpret_cast your void* back to memory_block<T>* and then static_cast that to special_block<T>* (assuming that you know for sure that the dynamic type of the complete object is actually special_block<T>). Unfortunately, as soon as NonTrivial enters the picture, all bets are off because NonTrivial has a virtual method and, thus, is not standard layout which also means that memory_block<NonTrivial> will not be standard layout…

One thing you could do is, e.g., have just a buffer to provide storage for a T in your memory_block and then construct the actual T inside the storage of data via placement new. for example:

#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();
    }

    …
};
Run Code Online (Sandbox Code Playgroud)

That way memory_block<T> will always be standard-layout…