通过指向第一个成员的指针访问未知大小的类成员数组

Access class member array of unknown size through pointer to first member

提问人:Lukas Lang 提问时间:5/25/2023 最后编辑:Lukas Lang 更新时间:5/26/2023 访问量:127

问:

我希望能够处理指向具有未知大小的数组成员的对象的指针,并通过指向其公共第一个成员的类型擦除指针访问该数组。我目前的尝试如下:

#include <cstddef>

struct node_base
{
    node_base* next;
    int size;
};

template <int n>
struct node
{
    node_base base;
    char data[n];

    node() : base{nullptr, n} {
        static_assert(offsetof(node, data) == sizeof(node_base));
    }
};

void process_queue(node_base* head)
{
    while (head)
    {
        for (int i = 0; i < head->size; ++i)
        {
            *(reinterpret_cast<char*>(reinterpret_cast<char*>(head) + sizeof(node_base)) + i) = i;
        }

        head = head->next;
    }
}

int main()
{
    node<3> a{};
    node<4> b{};
    node<2> c{};

    c.base.next = &b.base;
    a.base.next = &c.base;

    process_queue(&a.base);

    return a.data[2] + c.data[1];
}

此代码构建了一个类似队列的结构(节点,并以“a -> c -> b”的形式相互指向),并将指向第一个元素的指针传递给 。然后,该函数将遍历队列并访问直接存储在成员之后的数组,并写入值 ...到其条目中。abcprocess_queuenode<n>::datanode_base0n-1

挑战在于节点具有不同的类型,因此队列的指针指向实际节点的成员,我需要一些东西才能从那里获取实际数据。nextnode_base

尽管这似乎在成功返回期望值 3 的意义上起作用 (Godbolt),但我不确定这是否被允许。

问题

假设我通过某种方法知道指针指向具有大小数组的对象的第一个成员,通过上面的代码访问数组的元素是否合法?如果不是,它可以在不扩大规模的情况下合法化吗?curcur->sizenode<n>::datasizeof(node<n>)

C++ 数组 转换 语言-律师 类型-擦除

评论

0赞 Some programmer dude 5/25/2023
“好”的方式是正常的沮丧,比如.我猜你的实际代码不能这样做,因为它不知道大小(模板参数的值)?您能详细说明一下您试图解决的潜在和实际问题吗?为什么不知道何时需要访问数组的值?为什么要使用自己的数组和容器类而不是 or ?为什么要使用继承?你的真实代码中是否有虚函数(这可能会使你的计算全部错误)?static_cast<derived<5>*>(p)->c[2]nnstd::vectorstd::array
0赞 Lukas Lang 5/25/2023
@Someprogrammerdude我试图在问题底部解决您的问题,如果仍有不清楚之处,请告诉我
0赞 n. m. could be an AI 5/25/2023
base* p = &d;是相当没有意义的。对于任何操作,您始终需要向下转换为,但访问 .但是你不能问自己它是否是一个(没有虚拟),所以你需要将这些信息存储在其他地方。总而言之,你正在做的事情并不比.pderivedp[0]pderivedvoid* b = d;
1赞 n. m. could be an AI 5/25/2023
“在我需要访问数组时,我不知道派生类型的大小 n,因此正常的向下转换不起作用。”那么你要做的事情基本上是不可能的。你必须沮丧。没有办法绕过它。
0赞 Some programmer dude 5/25/2023
我认为,问题的根本原因不在于你的代码,而在于你的设计。继承是干什么用的?它应该解决需求和分析中的什么问题?

答:

1赞 user17732522 5/26/2023 #1

严格按照当前标准,它已经是未定义的行为,因为这里的指针算术:

reinterpret_cast<char*>(head) + sizeof(node_base)

未定义。 是指向对象的指针,该对象不能与该位置的任何对象进行指针互换。因此,也将是指向同一对象的指针。因此,指针算术是未定义的,因为表达式 () 的指向类型与指向对象 () 的实际类型不相似headnode_basecharreinterpret_cast<char*>(head)node_basecharnode_base


但是,您在此处强制转换的目的是更改指针值。您打算获取指向对象的对象表示形式的指针。强制转换为通常用于访问对象表示形式,但该标准实际上并未提供此功能。char*node<n>char*

提案 P1839 试图将这种预期行为纳入标准。由于多种原因,由于以下几个原因,它目前在修订P1839R5中的措辞仍然无法使您的程序得到很好的定义:

首先,因为只有才能获得指向对象表示的指针,如提案的限制部分所述。reinterpret_cast<unsigned char*>

即使有 ,该提案下仍然存在问题:unsigned char

您的类恰好是标准布局。这是实现这一目标的必要条件。如果它们不是标准布局,那么通常没有任何方法可以从一个成员的指针转到另一个成员的指针。

但是,标准布局可以保证对象与其第一个成员子对象的指针可互换。因此,根据该建议,是否将生成指向成员或对象的对象表示的第一个元素的指针是悬而未决的。这在提案中被指出为一个悬而未决的问题。node<n>reinterpret_cast<unsigned char*>(head)node_basenode<n>

但是,假设它确实生成了指向对象的对象表示的指针,那么下一个问题是是否也指向数组成员的对象表示。我不确定该提案的意图是什么。node<n>reinterpret_cast<unsigned char*>(head) + sizeof(node_base)) + icharnode<n>

但即使这不是问题,该提案也只定义了如何从对象表示中读取写信给它超出了范围,根据提案仍然是UB。

因此,至少需要保留外部并将其包装在调用中,以便获取指向对象本身的指针(而不是其对象表示或对象的对象表示)。reinterpret_cast<char*>std::laundercharnode<n>

评论

0赞 Lukas Lang 5/28/2023
感谢您的详细回答!两个问题:如果我将对象更改为有效的 ,它会“修复”事情吗,我将放置新分配对象和数组到其中?然后指针算术将有效地用于数组。(由于此时它与自定义分配器非常相似,因此在我看来,这可能是定义明确的)如果这还不够:将指向数据数组的指针存储在(在构造函数中初始化)并通过它访问数据是否合法?std::aligned_storagenode_basedata+sizeof(node_base)datanode_basenode<n>
0赞 user17732522 5/29/2023
@LukasLang 这将取决于你如何做到这一点,以及你想阅读标准的严格程度。通过指向数组的指针进行存储和访问是不行的,但是存储指向数组的第一个元素的指针并通过该指针进行访问是可以的,并且不应涉及任何强制转换。
0赞 Lukas Lang 5/29/2023
如果我不存储指针,而是从指针到缓冲区的开头重新计算它,我想在使用它之前我必须在指针上使用它?(因为据我了解,从技术上讲,只有 返回的指针才允许访问 分配在 的对象,对吧?std::laundernew (addr) ...addr
0赞 user17732522 5/29/2023
@LukasLang 是的,如果您使用缓冲区指针(并且缓冲区必须是 or 的数组),则可以在其中自由执行指针算术,但需要访问嵌套对象。 基本上已经在其返回值上为您做到了这一点。但是,我建议直接将 or 数组与属性一起使用以获得正确的对齐方式,而不是我建议。我相信你可以找到很多关于它的潜在问题/弃用的讨论。std::byteunsigned charstd::laundernewstd::launderstd::aligned_storageunsigned charstd::bytealignasstd::aligned_storage
0赞 user17732522 5/29/2023
@LukasLang 但是,从指向嵌套对象的指针到指向缓冲区元素的指针通常是不可能的,即使使用 .请参阅其前提条件。还要注意的是,除了与 /reference(子)对象相关的少数例外,据我所知,目前没有编译器实际使用这些类型的 UB 进行优化。因此,您已经比大多数人更担心细节。std::launderconst