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

Posted

技术标签:

【中文标题】是否将指向第一个成员的指针解释为类本身定义良好?【英文标题】:Is interpreting a pointer to first member as the class itself well defined? 【发布时间】:2019-09-21 01:42:30 【问题描述】:

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

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 :
        datastd::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) 行:我有一个指向dataNonTrivial 类型)的指针,它是memory_block&lt;NonTrivial&gt; 的成员。我知道void* 将始终指向memory_block&lt;T&gt; 的第一个数据成员。

那么将void* 投射到类的第一个成员到类中安全吗?如果没有,还有其他方法吗?如果它能让事情变得更简单,我可以摆脱继承。

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

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

我的静态断言:

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

【问题讨论】:

从技术上讲,我相信编译器会决定将成员变量放在哪里以及如何对齐它们。我不确定您是否可以认为这是一种保证。您可以添加一个static_assert 来检查sizeof(memory_block&lt;T&gt;) == sizeof(T),这可以为您提供保证:-) @Neijwiert 是和否。标准提供了一些保证。 对于标准布局类型,第一个成员是类的地址。在那之后,我们真的没有其他保证,除了成员按照声明的顺序排列在内存中,但是它们之间可以/将会有填充。 @FrançoisAndrieux 类型本身不是多态的,只有一个成员。有时。 如果您可以升级到 C++17,您可以使用 std::any 和带有 std::visit 的访问者模式来为您处理类型擦除和访问。 【参考方案1】:

只要memory_block&lt;T&gt; 是标准布局类型[class.prop]/3,memory_block&lt;T&gt; 的地址和它的第一个成员data 的地址就是指针可互转换的[basic.compound]/4.3。如果是这种情况,标准保证您可以reinterpret_cast 从指向另一个的指针中获取指向另一个的指针。只要您没有标准布局类型,就没有这样的保证。

对于您的特定情况,memory_block&lt;T&gt; 将是标准布局,只要 T 是标准布局。您的 special_block 永远不会是标准布局,因为它包含 std::vector(@NathanOliver 在下面的评论中也指出),这不能保证是标准布局。在您的情况下,由于您只需插入指向您的special_block&lt;T&gt;memory_block&lt;T&gt; 子对象的data 成员的指针,因此只要T 是标准布局,您仍然可以使其工作reinterpret_cast 您的void* 回到memory_block&lt;T&gt;* 然后static_castspecial_block&lt;T&gt;* (假设你确定完整对象的动态类型实际上是special_block&lt;T&gt;)。不幸的是,一旦NonTrivial 进入图片,所有的赌注都被取消了,因为NonTrivial 有一个虚拟方法,因此不是标准布局,这也意味着memory_block&lt;NonTrivial&gt; 不会是标准布局......

您可以做的一件事是,例如,只有一个缓冲区来为您的 memory_block 中的 T 提供存储空间,然后通过放置 new 在 data 的存储空间中构造实际的 T。例如:

#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&lt;T&gt; 将始终是标准布局……

【讨论】:

我做了一些测试,似乎memory_block&lt;T&gt; 可以,但special_block&lt;T&gt; 不行。如果我之前将指针投射到memory_block&lt;T&gt;,我还可以吗? 这是否意味着您必须先static_cast&lt;NonTrivial*&gt;(vec[1]) 然后再reinterpret_cast&lt;memory_block&lt;NonTrivial&gt;*&gt; @GuillaumeRacicot memory_block&lt;T&gt; 仅当 T 为标准布局时才为标准布局。由于NonTrivial 不是标准布局(它具有虚函数),因此不允许从NonTrivial* 转换为memory_block&lt;NonTrivial&gt;*。从memory_block&lt;NonTrivial&gt;*special_block&lt;NonTrivial&gt;* 与问题无关。或者至少我不明白怎么做。 可能还想指出special_block 从来都不是标准布局,因为它包含vector @NathanOliver 而且因为它本身和它的基类都有非静态成员。

以上是关于是否将指向第一个成员的指针解释为类本身定义良好?的主要内容,如果未能解决你的问题,请参考以下文章

C ++ typedef函数定义为类成员,以后用于函数指针?

如果它被定义为c ++类中的成员函数,我得到了“非标准语法;使用'&'创建指向成员的指针“[复制]

C++学习之旅第二站:类和对象进阶

获取指向 LLVM-IR 中数组第一个元素的指针

指向匿名联合成员的指针是不是相等?

指向不完整类型成员函数的指针