如果Derived没有向Base添加新成员(并且是POD),那么可以安全地执行什么样的指针转换和解除引用?

Aar*_*aid 6 c++ undefined-behavior language-lawyer

(这是关于未定义行为(UB)的另一个问题.如果这个代码在某些编译器上"起作用",那么在UB的土地上就没有任何意义.这是可以理解的.但正好在下面的哪一行我们会进入UB?)

(关于SO已经有很多非常相似的问题了,例如(1) 但是我很好奇在取消引用它们之前可以用指针安全地完成什么.)

从一个非常简单的Base类开始.没virtual办法.没有继承.(也许这可以扩展到任何POD?)

struct Base {
        int first;
        double second;
};
Run Code Online (Sandbox Code Playgroud)

然后是一个简单的扩展,添加(非virtual)方法,不添加任何成员.没有virtual继承.

struct Derived : public Base {
        int foo() { return first; }
        int bar() { return second; }
};
Run Code Online (Sandbox Code Playgroud)

然后,考虑以下几行.如果与定义的行为有一些偏差,我很想知道确切地知道哪些行.我的猜测是我们可以安全地对指针执行大部分计算.是否有可能这些指针计算中的一些,如果没有完全定义,至少会给我们某种"不确定/未指定/实现定义"的值,这些值并非完全没用?

void foo () {
    Base b;
    void * vp = &b;     // (1) Defined behaviour?
    cout << vp << endl; // (2) I hope this isn't a 'trap value'
    cout << &b << endl; // (3a) Prints the same as the last line?
                        // (3b) It has the 'same value' in some sense?
    Derived *dp = (Derived*)(vp);
                        // (4) Maybe this is an 'indeterminate value',
                        // but not fully UB?
    cout << dp << endl; // (5)  Defined behaviour also?  Should print the same value as &b
Run Code Online (Sandbox Code Playgroud)

编辑:如果程序在这里结束,它会是UB吗?请注意,在此阶段,除了将指针本身打印到输出之外,我还没有尝试过任何操作dp.如果简单的铸造是UB,那么我猜问题就在这里结束了.

                        // I hope the dp pointer still has a value,
                        // even if we can't dereference it
    if(dp == &b) {      // (6) True?
            cout << "They have the same value. (Whatever that means!)" << endl;
    }

    cout << &(b.second) << endl; (7) this is definitely OK
    cout << &(dp->second) << endl; // (8)  Just taking the address. Is this OK?
    if( &(dp->second) == &(b.second) ) {      // (9) True?
            cout << "The members are stored in the same place?" << endl;
    }
}
Run Code Online (Sandbox Code Playgroud)

我对上面的(4)有点紧张.但我认为对于void指针进行转换总是安全的.也许可以讨论这种指针的价值.但是,是否定义了执行转换,并将指针打印到cout

(6)也很重要.这会评估为真吗?

(8)中,我们第一次对该指针进行解引用(正确的术语?).但请注意,此行不读取dp->second.它仍然只是一个左值,我们采取其地址.我假设这个地址的计算是由我们从C语言得到的简单指针算术规则定义的?

如果以上所有都可以,也许我们可以证明这static_cast<Derived&>(b)是可以的,并且将导致一个完全可用的对象.

Mat*_*lia 4

    \n
  1. 从数据指针到 的转换void *始终保证有效,并且保证指针在往返Base *-> void *-> Base *(C++11 \xc2\xa75.2.9 \xc2\xb613) 中存活下来;
  2. \n
  3. vp是一个有效的指针,所以应该没有任何问题。
  4. \n
  5. A。尽管打印指针是实现定义的1,但打印的值应该相同:事实上,operator<<默认情况下仅重载于const void *,因此当您编写时cout<<&b,您将转换为const void *无论如何operator<<,即在两种情况下看到的都会&b转换为const void *

    \n\n

    b. 是的,如果我们采用“具有相同值”的唯一合理定义 - 即它与运算符进行比较==;事实上,如果您比较vp&b==结果是true,如果您转换vpBase *(由于我们在 1 中所说的),还是转换&bvoid *

    \n\n

    这两个结论都来自 \xc2\xa74.10 \xc2\xb62,其中指定任何指针都可以转换为void *(对通常的cv限定的东西取模),并且结果 \xc2\xab 指向对象[...]所在的存储位置\xc2\xbb 1

  6. \n
  7. 这很棘手;C 风格的强制转换相当于 a static_cast,这将很高兴地允许将 \xc2\xab"指向cv1 B [...] 的指针强制转换为 [...] "指向 *cv2 的指针D",其中是从\xc2D派生的类B\xbb (\xc2\xa75.2.9, \xc2\xb611;还有一些额外的约束,但这里满足了);但是

    \n\n
    \n

    如果指向cv1 B \xe2\x80\x9d 的\xe2\x80\x9c 类型指针的纯右值指向的 aB实际上是类型 的对象的子对象D,则生成的指针指向类型 的封闭对象D否则,转换的结果是未定义的。

    \n
    \n\n

    (强调已添加)

    \n\n

    所以,在这里你的演员是允许的,但结果是未定义的......

  8. \n
  9. ...这导致我们打印它的值;由于转换的结果未定义,因此您可能会得到任何结果。由于指针可能允许具有陷阱表示(至少在 C99 中,我只能在 C++11 标准中找到对“陷阱”的稀疏引用,但我认为这种行为可能应该已经从 C89 继承),您甚至可以只需读取此指针并通过 打印它就会发生崩溃operator<<

  10. \n
\n\n

如果遵循,则 6、8 和 9 也没有意义,因为您使用的是未定义的结果。

\n\n

此外,即使强制转换有效,严格别名(\xc2\xa73.10、\xc2\xb610)也会阻止您对指向的对象执行任何有意义的操作,因为仅当动态类型时才允许Base通过指针对对象进行别名对象DerivedBase实际上是Derived; 任何偏离 \xc2\xa73.10 \xc2\xb610 中指定的异常的情况都会导致未定义的行为。

\n\n
\n\n

笔记:

\n\n
    \n
  1. operator>>委托到num_put概念上委托给printfwith %p,其描述归结为“实现定义”。

  2. \n
  3. 这消除了我的担心,即理论上,当转换为 时,邪恶的实现可能会返回不同但相同的值void *

  4. \n
\n