7

(这是关于未定义行为 (UB) 的另一个问题。如果此代码在某些编译器上“工作”,那么这在 UB 领域毫无意义。这是可以理解的。但是我们到底在下面哪一行进入 UB?)

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

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

struct Base {
        int first;
        double second;
};

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

struct Derived : public Base {
        int foo() { return first; }
        int bar() { return second; }
};

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

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

编辑:如果程序在这里结束,会是 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;
    }
}

我对上面的(4)有点紧张。但我认为在 void 指针之间进行转换总是安全的。也许可以讨论这样一个指针的值。但是,是否定义为进行强制转换并将指针打印到cout?

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

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

如果以上都OK,也许我们可以证明它static_cast<Derived&>(b)是OK的,并且会导致一个完全可用的对象。

4

2 回答 2

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

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

    这两个结论都来自 §4.10 ¶2,其中指定任何指针都可以转换为void *(以通常的cv限定的东西为模),结果«指向对象 [...] 的存储位置的开始居住» 1

  4. 这很棘手;C 风格的强制转换等效于 a static_cast,这将很高兴允许将“指向cv1 B [...] 的指针转换为 [...]”指向 *cv2的指针D,其中D是派生自B» 的类(第 5.2 节。 9, ¶11; 有一些额外的限制,但在这里满足);但是

    如果类型“指向cv1 B的指针”的纯右值指向B实际上是类型对象的子对象的 a D,则生成的指针指向类型的封闭对象D否则,强制转换的结果是未定义的。

    (重点补充)

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

  5. ...这导致我们打印它的价值;由于演员表的结果是未定义的,你可能会得到任何东西。由于指针可能允许具有陷阱表示(至少在 C99 中,我只能在 C++11 标准中找到对“陷阱”的稀疏引用,但我认为这种行为可能应该已经从 C89 继承)你甚至可以只需读取此指针以通过operator<<.

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

此外,即使强制转换是有效的,严格的别名(§3.10,¶10)也会阻止你对指向的对象做任何有意义的事情,因为只有当对象的动态类型实际上是;时才允许Base通过指针对对象进行别名。任何偏离 §3.10 ¶10 中指定的异常的东西都会导致未定义的行为。DerivedBaseDerived


笔记:

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

  2. 这排除了我担心一个邪恶的实现在理论上可能会在转换为时返回不同但等效的值void *

于 2013-11-02T15:51:16.103 回答
1

(试图从严格别名的角度回答我自己的问题。一个好的优化器有权做一些意想不到的事情,这实际上给了我们 UB。但无论如何我都不是专家!)

在这个函数中,

 void foo(Base &b_ref) {
     Base b;
     ....
 }

很明显,b不能b_ref互相引用。这个特定的例子不涉及兼容类型的分析,它只是一个简单的观察,一个新构造的局部变量保证是对它自己的唯一引用。这允许优化器做一些技巧。它可以存储b在一个寄存器中,然后它可以执行代码,例如b_ref.modify(),修改,在不受影响b_ref的知识中安全。b(也许只有真正聪明的优化器会注意到这一点,但这是允许的。)

接下来,考虑这个,

void foo(Base &b_ref, Derived&d_ref);

在此函数的实现中,优化不能假定 b_ref 和 d_ref 引用不同的对象。因此,如果代码调用d_ref.modify(),那么下次代码访问b_ref时必须再次查看存储b_ref对象的内存。如果 CPU 寄存器中存在数据副本b_ref,则可能是过期数据。

但是如果这些类型彼此无关,那么这样的优化是允许的。例如

struct Base1 { int i; };  struct Base2 { int i; };
void foo(Base1 & b1_ref, Base2 &b2_ref);

可以假设这些指向不同的对象,因此允许编译器做出某些假设。 b2_ref.i=5;不能改变b1_ref.i,因此编译器可以做出一些假设。(实际上,可能还有其他线程,甚至是 POSIX 信号,在幕后进行更改,我必须承认我不会清除线程!)

因此,允许编译器进行优化的假设。考虑一下:

Base b; // a global variable
void foo() {
    Derived &d_ref = some_function();
    int x1 = b.i;
    d_ref.i = 5;
    int x2 = b.i;
}

这样,优化器就知道 的动态类型b,它是Base. 两次连续调用b.i应该给出相同的值(除了其他线程或其他),因此允许编译器将后者优化为int x2 = x1. 如果some_function返回 a Base&,即Base &d_ref = some_function();不允许编译器进行这样的优化。

因此,给定编译器知道其动态类型为 的对象Base,以及对派生类型的引用Derived&,编译器有权假定它们引用不同的对象。允许编译器稍微重写代码,假设两个对象不相互引用。这至少会导致不可预测的行为。您所做的任何违反优化器允许做出的假设的行为都是未定义的行为。

于 2013-11-02T15:40:28.553 回答