当前位置: 首页 > 知识库问答 >
问题:

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

陶琦
2023-03-14

下面是一个示例:

#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的意义是什么),但我不确定。

最近我被告知它实际上是UB,所以我想一劳永逸地弄清楚它。

上面的例子到底有没有引起UB?如果你把类修改成非标准布局,会影响结果吗?

如果是UB,是否有任何解决方法(例如,应用< code>std::launder)?

这整个主题似乎没有实际意义,而且没有得到充分的阐述。

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

>

  • 添加到“char*”指针UB时,它实际上没有指向char数组吗2011年,CWG确认允许我们通过无符号字符指针检查标准布局对象的表示。

    >

  • 不清楚是否可以使用char指针,常识说可以。
  • 不清楚从C 17std::launder是否需要应用于(无符号char*)强制转换的结果。考虑到这将是一个突破性的变化,它可能是不必要的,至少在实践中是这样。

    不清楚为什么C17将offsetof更改为有条件地支持非标准布局类型(以前是UB)。这似乎意味着如果一个实现支持它,那么它还允许您通过unsignedchar*检查非标准布局对象的表示。

    当在标准布局对象中进行指针运算时,我们需要使用std::launder吗?——类似这个的问题。没有给出明确的答案。

  • 共有2个答案

    钱志
    2023-03-14

    在我看来,这个问题和另一个关于launder的问题都可以归结为对C17[expr.static.cast]/13最后一句话的解释,它涵盖了static_cast的情况

    “pointer-to-cv1void”类型的prvalue可以转换为“pointer-to-cv2T”类型的prvalue,

    […]

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

    有些海报似乎认为这意味着强制转换的结果不能指向T类型的对象,因此,带指针或引用的reinterpret_cast只能用于指针可转换类型。

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

    • CWG1314决议被推翻
    • 无法检查标准布局对象的任何字节(因为转换为无符号字符*或任何可能无法用于访问该字节的字符类型)
    • 严格的别名规则是多余的,因为实际实现此类别名的唯一方法是使用此类强制转换
    • 没有规范性文本来证明注释“[注:将“指针到T1”类型的prvalue转换为“指针到T2”类型(其中T1和T2是对象类型,T2的对齐要求并不比T1更严格),并返回其原始类型会产生原始指针值。-结束注]”
    • offsetof是无用的(因此对它的C17更改是多余的)

    对我来说,这句话似乎是一个更合理的解释,即强制转换的结果指向内存中与操作数相同的字节。(与指向其他字节相反,这句话未涵盖的一些指针强制转换可能会发生)。说“值不变”并不意味着“类型不变”,例如,我们将从intlong的转换描述为保留值。

    此外,我想这可能会引起一些争议,但我认为如果指针的值是对象的地址,那么指针指向该对象,除非标准特别排除这种情况。

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

    似乎没有任何其他显式语句来定义指针何时可以或不能指向对象,但 basic.compound/3 表示所有指针都必须是以下四种情况之一(指向对象、点超过末尾、null、无效)。

    排除的案例包括:

    • std::launder的用例专门解决了这样一种情况,即有这样一种语言排除了使用未清洗的指针。
    • 过去的结束指针不指向对象。(basic.compound/3)

    商柏
    2023-03-14

    这里我将提到C 20(草案)的措辞,因为一个相关的编辑问题在C 17和C 20之间得到了解决,也有可能提到C 20草案HTML版本中的具体句子,但除此之外,与C 17相比没有什么新的内容。

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

    指针类型的每个值都是下列值之一:
    — 指向对象或函数的指针(该指针称为指向对象或函数),或者
    — 经过对象末尾的指针 ([expr.add]),或
    — 该类型的空指针值,或
    — 无效的指针值。

    现在,让我们看看(char*)中发生了什么

    我不想证明a是一个左值,它表示类型a»来引用这个对象。

    的含义

    如果操作数是类型T的左值,则结果表达式是类型为“指针到T”的prvalue,其结果是指向指定对象的指针

    所以,

    现在,转换(char*)

    转换为< code>void*不会改变指针值([conv.ptr]/2):

    类型为“pointer-to-cvT”的prvalue,其中T是对象类型,可以转换为类型为“指针到cvvoid”的prvalue。指针值([basic.compound])在此转换中保持不变。

    即它仍然是“指向(对象)a”的指针。

    [expr.static.cast]/13覆盖外static_cast

    “pointer-to-cv1void”类型的prvalue可以转换为“pointer-to-cv2T>”类型prvalue,其中 是对象类型,cv2与cv1具有相同的cv限定条件,或大于cv1。如果原始指针值代表内存中字节的地址A,而A不满足 的对齐要求,则生成的指针值未指定。否则,如果原始指针值指向一个对象a,并且存在一个 T类型的对象b(忽略cv限定),该对象b可以与a进行指针互换,则结果是指向b的指针。否则,指针值将通过转换保持不变。

    没有类型为char的对象可以与对象a([basic.compound]/4)进行指针互转换:

    两个对象 a 和 b 是指针可互转换的,如果:
    — 它们是同一个对象,或者
    — 一个是联合对象,另一个是该对象的非静态数据成员 ([class.union]),或者
    — 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员, 该对象的任何基类子对象 ([class.mem]),或者
    — 存在一个对象 c,使得 a 和 c 是指针可互换的,而 c 和 b 是指针可互换的。

    这意味着< code>static_cast

    所以,(char*)

    对于加法或减法,如果表达式PQ的类型为“指针指向cvT”,其中T和数组元素类型不相似,则行为未定义。

    出于指针算术的目的,对象a被认为是数组A[1]([basic.compound]/3)的元素,因此数组元素类型为A,指针表达式P的类型为“指针指向char”,charA不是相似的类型(参见[conv.qual]/2),因此行为是未定义的。

     类似资料:
    • 这个问题的后续问题 我们有以下代码: 这种访问是一种未定义的行为吗?一方面,不需要对象来访问静态类成员,另一方面,

    • 在这两个示例中,通过偏移其他成员的指针来访问结构成员是否会导致未定义/未指定/实现定义的行为? C11§6.7.2.1第14段似乎表明,这应该是实施定义: 结构或联合对象的每个非位字段成员都以适合其类型的实现定义方式对齐。 后来又说: 结构对象中可能有未命名的填充,但在其开头没有。 但是,如下所示的代码似乎相当常见: 该标准似乎保证 与 和< code >( 原始应用程序正在考虑从一个结构字段到另

    • GCC 8.2.1和MSVC 19.20编译以下代码,但Clang 8.0.0和ICC 19.0.1无法这样做。 Clang 8.0.0 的错误消息如下: 我注意到它在两种情况下可以用Clang编译: 从最后一个定义中删除括性 将线 替换为

    • 假设有一个结构 我得到一个指向该结构的成员的指针,比如作为某个函数的参数: 如何获取指向包含对象的指针?最重要的是:在不违反标准中的某些规则的情况下,也就是说,我想要标准定义的行为,而不是未定义或实现定义的行为。 附带说明:我知道这规避了类型安全。

    • 我在一个类中使用了几个函数,这些函数通过函数接口传递给ostream,而函数接口又可以用来输出错误消息。我曾希望能够将所有ostream绑定到一个对象,然后在必要时重定向到一个文件。 我的代码的相关部分如下所示: 在构造函数(或程序中的所有构造函数)的第一个花括号中,我得到以下错误消息: 受保护功能“std::basic\u ostream 或者对于我粘贴在此处的简短示例代码: “std::bas

    • 问题 你已经获得了一个被编译函数的内存地址,想将它转换成一个Python可调用对象, 这样的话你就可以将它作为一个扩展函数使用了。 解决方案 ctypes 模块可被用来创建包装任意内存地址的Python可调用对象。 下面的例子演示了怎样获取C函数的原始、底层地址,以及如何将其转换为一个可调用对象: >>> import ctypes >>> lib = ctypes.cdll.LoadLibrar