(这是有关未定义行为(UB)的另一个问题。如果此代码在某些编译器上“起作用”,则在UB领域中没有任何意义。这是可以理解的。但是,我们到底在哪一行进入UB?)

(SO上已经有很多非常相似的问题,例如(1),但我很好奇在取消引用指针之前可以安全地完成哪些操作。)

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

struct Base {
        int first;
        double second;
};

然后是一个简单的扩展,它添加了(non-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)感到有些紧张。但是我认为在空指针之间进行强制转换始终是安全的。也许可以讨论这样一个指针的值。但是,是否定义了进行强制类型转换以及将指针打印到cout的定义?

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

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

如果以上所有操作都可以,那么我们也许可以证明static_cast<Derived&>(b)是可以的,并且会导致一个完美可用的对象。

最佳答案

  • 始终保证从数据指针到void *的转换都可以正常工作,并且保证指针在往返Base *-> void *-> Base *的过程中仍然有效(C++ 11§5.2.9¶13);
  • vp是有效的指针,因此应该没有任何问题。
  • 尽管打印指针是实现定义的,但打印的值应该相同:实际上,默认情况下,operator<<仅对const void *重载,因此,当您编写cout<<&b时,无论如何都将转换为const void *,即,两种情况下operator<<看到的都是&b转换为const void *

    b。是的,如果我们采用“具有相同值”的唯一明智的定义-即它与==运算符进行比较;实际上,如果将vp&b==进行比较,则结果是true,如果您将vp转换为Base *(由于我们在1中所说),又将&b转换为void *

    这两个结论均来自§4.10¶2,其中指定了任何指针都可以转换为void *(对通常的cv限定的东西进行模运算),结果«指向对象[...]的存储位置的开始。 ]居住»1
  • 这很棘手; C样式转换等效于static_cast,这将很乐意允许将«“指向cv1 B的指针[...]转换为” * cv2 D的指针“,其中D是派生自B的类»(第5.2.9节,¶11;还有一些其他约束,但是在这里可以满足);但是:



    (强调)

    因此,这里允许您进行强制类型转换,但是结果是不确定的...
  • ...导致我们打印它的值(value);由于强制转换的结果是不确定的,因此您可能会得到任何结果。由于可能允许指针具有陷阱表示形式(至少在C99中,我只能在C++ 11标准中找到对“陷阱”的稀疏引用,但我认为这种行为可能应该已经从C89继承了),甚至仅通过读取此指针以通过operator<<进行打印即可导致崩溃。

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

    另外,即使强制转换有效,严格的别名(第3.10节,第10节)也会阻止您对指向的对象执行任何有意义的操作,因为仅当Base对象的动态类型时才允许通过Derived指针对Base对象进行别名实际上是Derived;偏离§3.10¶10中指定的异常的任何结果都将导致未定义的行为。

    笔记:
  • operator>>委托(delegate)给num_put,后者从概念上委托(delegate)给printf%p,其描述可以归结为“实现定义”。
  • 排除了我的担心,即当转换为void *时,邪恶的实现理论上可能返回不同但等效的值。
  • 关于c++ - 如果“派生”没有向Base添加新成员(并且是POD),那么可以安全地进行哪种类型的指针强制转换和取消引用?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/19741843/

    10-11 21:57
    查看更多