从成员指针获取对象指针

25

假设有一个结构

struct Thing {
  int a;
  bool b;
};

假设有一个结构体,我得到了该结构体成员变量 b 的指针,比如作为某个函数的参数:


void some_function (bool * ptr) {
  Thing * thing = /* ?? */;
}

如何获取指向包含对象的指针? 最重要的是:在不违反标准规则的情况下,我想要标准定义的行为,而不是未定义或实现定义的行为。

顺便提一下:我知道这种做法绕过了类型安全检查。


2
从一个布尔地址中推断出您的“Thing”指针是棘手的。为什么不将Thing地址/指针传递给您的函数呢? - Stefan
1
@Stefan 这应该是分配器的一部分:它有带有簿记和数据的节点,我在分配时分发数据指针,并在释放时将其返回。 - Daniel Jour
我刚注意到你在这里关于这个分配器的评论。这意味着你不能保证它是标准布局,而且 offsetof 可能是未定义的。自 2015 年 11 月以来,你可能已经放弃了这个问题,但如果它仍然相关,并且出现了一个非 offsetof 的答案,我建议你实现它。幸运的是,对于一个分配器,你不需要返回一个指针。你可以返回任何满足 NullablePointerRandomAccessIterator 的指针类结构,并将其 typedefpointer - Daniel H
只是好奇:如果您需要从某个成员获取对象,为什么不能简单地将对象作为指针传递而不是成员? - Aconcagua
4个回答

22

如果您确定指针确实指向结构体中的成员 b,就像某人所做的那样

Thing t;
some_function(&t.b);

然后您应该能够使用offsetof宏来获取指向结构体的指针:

std::size_t offset = offsetof(Thing, b);
Thing* thing = reinterpret_cast<Thing*>(reinterpret_cast<char*>(ptr) - offset);

请注意,如果指针ptr实际上没有指向Thing::b成员,那么如果您使用指针thing,上述代码将导致未定义的行为。

3
据我所知,char* 是这种指针算术运算的完全安全和规范方式。 - Karoly Horvath
2
好的,我有点困惑了 :) char 不能小于8位,因为最小范围,如果它超过8位,int8_t甚至无法存在,因为地址和sizeof等等...一切都很好。 - deviantfan
11
“使用 int8_t 的实际用途在于它被定义为一个字节,而 char 可能不是。”——不对,你完全颠倒了。在这里应该使用 charint8_t 是错误的选择。 - Konrad Rudolph
3
请注意,offsetof对于非标准布局类型具有未定义的行为(C++11)。实际上,它通常仍然按预期工作。 - davmac
9
“标准布局”一词大致意思是“不使用C语言没有的任何特性”。事实证明,offsetof实际上是一个C语言的特性,而不是C++语言的特性。C++规范明确表示,在非标准布局的类上使用offsetof是未定义的。请参见std::is_standard_layoutoffsetof以及非静态数据成员页面上的标准布局部分 - Daniel H
显示剩余11条评论

6
X* get_ptr(bool* b){
    static typename std::aligned_storage<sizeof(X),alignof(X)>::type buffer;

    X* p=static_cast<X*>(static_cast<void*>(&buffer));
    ptrdiff_t const offset=static_cast<char*>(static_cast<void*>(&p->b))-static_cast<char*>(static_cast<void*>(&buffer));
    return static_cast<X*>(static_cast<void*>(static_cast<char*>(static_cast<void*>(b))-offset));
}

首先,我们创建一些静态存储空间,可以容纳一个X对象。然后,我们获取可能存在于缓冲区中的X对象和其b元素的地址。通过将其转换回char*,我们可以获得缓冲区内bool的偏移量,然后使用该偏移量将指针调整为指向包含X的实际bool指针。

你的意思是将buffer声明为static typename std::aligned_storage<sizeof(X),alignof(X)>::type吗?此外,通过void*进行static_cast而不是直接进行reinterpret_cast转换为char*X*是否有原因? - Daniel H
是的,很好发现。我不喜欢reinterpret_cast,因为映射未经明确定义。static_cast有一个定义良好的映射。 - Anthony Williams
我想这通常是有道理的,尽管指针的reinterpret_cast是定义良好的。我不喜欢这会保留额外的空间,但除此之外似乎还不错。我稍微担心偏移量在这个X和参数中的那个X之间不同,但如果编译器这样做了(对于非标准布局,我找不到任何证据表明它不能,但我怀疑任何编译器都不会),我不确定是否有任何方法可以避免它。 - Daniel H
很抱歉昨天没能及时分配完整的赏金;这仍然似乎是最好的解决方案。虽然我喜欢@rfb为此编写通用函数,但由于对齐问题,我不确定它是否符合标准。 - Daniel H
抱歉,如果我的答案有价值,我认为它不是为了普遍意义,而是因为它提出了一个由类数据成员指针提供的内置位移机制的想法。我的解决方案与@AnthonyWilliams的回复相同,它不是使用一个存储器,而是利用了这样一个偏移量的知识。实际上,我所引用的主要帖子证实了围绕着如何在没有临时实例的情况下获取成员指针的偏移量的问题。 - rfb
显示剩余2条评论

5
void some_function (bool * ptr) {
  Thing * thing = (Thing*)(((char*)ptr) - offsetof(Thing,b));
}

我认为没有未定义行为。


3
我的建议来自于Offset from member pointer without temporary instance中@Rod的回答,以及Offset of pointer to member中相似的@0xbadf00d的回答。
我开始想象一种偏移量来驱动指向类数据成员的指针的实现,后来通过相关帖子和我所做的测试得到了确认。
我不是C++从业者,因此对于简洁性表示抱歉。
#include <iostream>
#include <cstddef>

using namespace std;

struct Thing {
    int a;
    bool b;
};

template<class T, typename U>
std::ptrdiff_t member_offset(U T::* mem)
{
    return 
    ( &reinterpret_cast<const char&>( 
        reinterpret_cast<const T*>( 1 )->*mem ) 
      - reinterpret_cast<const char*>( 1 )      );
}

template<class T, typename U>
T* get_T_from_data_member_pointer (U * ptr, U T::*pU) {
  return reinterpret_cast<T*> (
      reinterpret_cast<char*>(ptr) 
    - member_offset(pU));
}

int main()
{

    Thing thing;
    thing.b = false;

    bool * ptr = &thing.b;
    bool Thing::*pb = &Thing::b;

    std::cout << "Thing object address accessed from Thing test object lvalue; value is: " 
        << &thing << "!\n";     
    std::cout << "Thing object address derived from pointer to class member; value is: " 
        << get_T_from_data_member_pointer(ptr, &Thing::b) << "!\n";    
}

使用1作为指针是否符合标准,或者可能存在对齐问题或其他问题?我认为除非该值是从指针原始的一组有限操作计算出来的,否则不允许将任何int转换为指针,尽管可能只是不允许解引用它。虽然在研究这个问题时,我多次想到希望C++默认提供所有类型TUT* operator-(U*, U T::*) - Daniel H
或者至少gcc提供了这个扩展,因为我非常确定它实际上将指向数据成员变量存储为ptrdiff_t(https://refspecs.linuxfoundation.org/cxxabi-1.86.html)。 - Daniel H
我认为这可能不合规,就像大多数reinterpret_cast问题一样,如果我记得我早期学习C++98的时候是正确的。 但是转换很棘手,因为它只是一种推断位移的方法,假设类数据成员指针是用偏移/位移实现的。 它只是为计算指针算术提供基础,没有解引用的内容。 如果我错了,请纠正我。 @Rod的答案指出了那个特定的复杂语法(首先是char引用,然后是取地址)是由于编译器警告而产生的,与您的观察相似。 - rfb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接