代码之家  ›  专栏  ›  技术社区  ›  HolyBlackCat

UB是通过将对象指针强制转换为“char*”,然后执行“*(member_type*)(指针+偏移量)”来访问成员的吗?

  •  6
  • HolyBlackCat  · 技术社区  · 4 年前

    这里有一个例子:

    #include <cstddef>
    #include <iostream>
    
    struct A
    {
        char padding[7];
        int x;
    };
    constexpr int offset = offsetof(A, x);
    
    int main()
    {
        A a;
        a.x = 42;
        char *ptr = (char *)&a;
        std::cout << *(int *)(ptr + offset) << '\n'; // Well-defined or not?
    }
    

    我一直认为它是定义明确的(否则这有什么意义呢 offsetof ),但不确定。

    最近我 was told 事实上,这是UB,所以我想一劳永逸地弄清楚。

    上述示例是否会导致UB?如果将类修改为非标准布局,是否会影响结果?

    如果是UB,是否有任何解决方法(例如申请 std::launder )?


    整个话题似乎没有意义,也没有明确说明。

    以下是我找到的一些信息:

    0 回复  |  直到 4 年前
        1
  •  6
  •   Language Lawyer    4 年前

    在这里,我将参考C++20(草案)的措辞,因为 one relevant editorial issue 在C++17和C++20之间是固定的,也可以在C++20草案的HTML版本中引用特定的句子,但除此之外,与C++17相比没有什么新东西。

    首先,指针值的定义 [basic.compound]/3 :

    指针类型的每个值都是以下值之一:
    指针指向 对象或函数(指针被称为指向对象或函数),或
    指针越过终点 对象([expr.add]),或
    这个 空指针值 对于该类型,或
    指针值无效 .

    现在,让我们看看发生了什么 (char *)&a 表达。

    让我不要证明这一点 a 是一个左值,表示类型的对象 A ,我会说对象 以引用此对象。

    的意义 &a 子表达式包含在 [expr.unary.op]/(3.2) :

    如果操作数是类型为的左值 T ,得到的表达式是一个类型为指针的pr值 T 其结果是指向指定对象的指针

    所以, &一 是类型为的pr值 A* 与价值 指针指向 (对象) .

    现在,演员阵容 (char*)&一 相当于 reinterpret_cast<char*>(&a) ,其定义为 static_cast<char*>(static_cast<void*>(&a)) ( [expr.reinterpret.cast]/7 ).

    演员阵容 void* 不会更改指针值( [conv.ptr]/2 ):

    指向的类型指针的prvalue cv T ,在哪里 T 是一个对象类型,可以转换为指针类型的prvalue cv void 。指针值([basic.compound])不会因此转换而改变。

    即它仍然 指针指向 (对象) .

    [expr.static.cast]/13 覆盖外层 static_cast<char*>(...) :

    指向的类型指针的prvalue cv1 无效 可以转换为指针类型的pr值 cv2 T ,在哪里 T 是一种对象类型,并且 cv2 简历资格与简历资格相同或更高, cv1 . 如果原始指针值表示内存中一个字节的地址A,而A不满足对齐要求 T ,则结果指针值未指定。 否则,如果原始指针值指向一个对象 ,并且存在一个对象 b 类型 T (忽略cv限定),指针可与 ,结果是一个指向 b . 否则,指针值不会因转换而改变。

    没有类型的对象 char 该指针可与对象相互转换 ( [basic.compound]/4 ):

    两个物体 b 指针可相互转换 如果:
    它们是同一个对象,或者
    一个是联合对象,另一个是该对象的非静态数据成员([class.union]),或
    一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据构件,则该对象的任何基类子对象([class.mem]),或
    存在一个对象 c 这样 c 指针可相互转换,以及 c b 指针可相互转换。

    这意味着 static_cast<char*>(...) 不会改变指针值,它与操作数中的指针值相同,即: 指针指向 .

    所以, (char*)&一 是类型为的pr值 char* 其值为 指针指向 。此值存储在 char* ptr 变量。然后,当你试图用这样的值进行指针运算时,即 ptr + offset ,你走进 [expr.add]/6 :

    对于加法或减法,如果表达式 P Q 具有指向的类型指针 cv T ,在哪里 T 并且数组元素类型不相似,行为未定义。

    为了指针运算的目的,对象 被视为数组的一个元素 A[1] ( [basic.compound]/3 ),因此数组元素类型为 A. ,指针表达式的类型 P 是指针指向 烧焦 , 烧焦 A. 不是相似的类型(参见 [conv.qual]/2 ),因此行为未定义。

        2
  •  3
  •   M.M    4 年前

    这个问题,另一个关于 launder 在我看来,两者都归结为对C++17[expr.static.cast]/13最后一句的解释,它涵盖了 static_cast<T *> 应用于指向正确对齐的无关类型的指针的操作数:

    指向的类型指针的prvalue cv1 void 可以转换为指针类型的pr值 cv2 T ,

    [...]

    否则,指针值不会因转换而改变。

    一些海报似乎认为这意味着演员阵容的结果不能指向某种类型的物体 T 因此 reinterpret_cast 带指针或引用只能用于指针可相互转换的类型。

    但我不认为这是合理的,而且(这是一个 荒谬还原论 论点),这一立场也意味着:

    • CWG1314的决议被推翻。
    • 无法检查标准布局对象的任何字节(因为转换为 unsigned char * 或者任何假定不能用于访问该字节的字符类型)。
    • 严格的混叠规则是多余的,因为实际实现这种混叠的唯一方法是使用这种转换。
    • 没有规范性文本来证明注释“[注:将指向T1的类型指针的prvalue转换为指向T2的类型指针(其中T1和T2是对象类型,T2的对齐要求不比T1严格),并返回到其原始类型,会得到原始指针值。结束注释]”
    • offsetof 毫无用处(因此C++17对它的更改是多余的)

    在我看来,这句话意味着强制转换的结果指向内存中与操作数相同的字节,这似乎是一种更明智的解释。(与指向其他字节相反,对于本句未涵盖的某些指针转换可能会发生这种情况)。说“值不变”并不意味着“类型不变”,例如,我们描述了从 int long 以保持价值。


    此外,我想这可能对一些人有争议,但我认为这是公理,如果指针的值是对象的地址,那么指针 指向 除非标准明确排除这种情况。

    这与[basic.compound]/3的文本是一致的,后者说的是相反的,即如果指针指向一个对象,那么它的值就是该对象的地址。

    似乎没有任何其他明确的语句定义指针何时可以或不能指向对象,但basic.compound/3说所有指针必须是四种情况之一(指向对象、指向末尾、null、无效)。

    排除的案例包括:

    • 用例 std::launder 特别解决了这样一种情况,即有这样的语言排除了使用未清洗指针的可能性。
    • 过终点指针不指向对象。(基本化合物/3)