如果 Derived 没有向 Base 添加新成员(并且是 POD),则可以安全地完成哪种指针强制转换和取消引用
If Derived adds no new members to Base (and is POD), then what kind of pointer casts, and dereferencing, can be safely done?
(这是关于未定义行为(UB)的另一个问题。 如果这段代码在某个编译器上"工作",那么这在UB的土地上没有任何意义。 这是可以理解的。 但是我们究竟在下面的哪条线上进入 UB?
(关于 SO 已经有许多非常相似的问题,例如 (1) 但我很好奇在取消引用指针之前可以安全地使用这些指针。
从一个非常简单的基类开始。 没有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)有点紧张。 但我认为在空指针之间投射总是安全的。 也许可以讨论这种指针的值。 但是,它是否定义为进行强制转换,并打印指向cout
的指针?
(6)也很重要。 这会评估为真吗?
在 (8) 中,我们第一次取消引用此指针(正确的术语? 但请注意,此行不会从dp->second
读取。 它仍然只是一个左值,我们采用它的地址。 我认为,这种地址计算是由我们从 C 语言中获得的简单指针算术规则定义的?
如果以上所有内容都没问题,也许我们可以证明static_cast<Derived&>(b)
是可以的,并且将导致一个完全可用的对象。
- 从数据指针到
void *
的转换始终保证有效,并且指针保证在往返Base *
->void *
->Base *
(C++11 §5.2.9 ¶13
); -
vp
是一个有效的指针,所以应该没有任何问题。 A. 尽管打印指针是实现定义的1,但打印的值应该是相同的:事实上,默认情况下
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
,这将愉快地允许铸造一个 «"指向 cv1B
的指针 [...]到 [...]"指向 *cv2D
的指针",其中D
是从B
» 派生的类 » (§5.2.9, ¶11;还有一些额外的约束,但在这里得到满足);但是:如果 prvalue 的类型 "指针指向 cv1
B
" 指向一个实际上是类型D
对象的子对象的子对象B
,则生成的指针指向类型D
的封闭对象。否则,转换的结果是未定义的。(着重号后加)
所以,这里允许你的演员表,但结果是不确定的......
。这导致我们打印它的价值;由于强制转换的结果是未定义的,因此您可能会得到任何东西。由于指针可能被允许具有陷阱表示(至少在 C99 中,我只能在 C++11 标准中找到对"陷阱"的稀疏引用,但我认为这种行为可能应该已经继承自 C89)您甚至可能通过读取此指针通过
operator<<
打印它而崩溃。
如果遵循,则 6、8 和 9 也没有意义,因为您使用的是未定义的结果。
此外,即使强制转换有效,严格的混叠(§3.10,¶10)也会阻止你对指向的对象做任何有意义的事情,因为只有当Base
对象的动态类型实际Derived
时,才允许通过Derived
指针为Base
对象锯齿;任何偏离§3.10 ¶10中指定的异常都会导致未定义的行为。
笔记:
-
operator>>
委托给num_put
,在概念上委托给printf
与%p
,其描述归结为"实现定义"。 -
这排除了我的担忧,即邪恶的实现在投射到
void *
时理论上可能会返回不同但等效的值。
(试图从严格混叠的角度回答我自己的问题。 一个好的优化器有权做一些意想不到的事情,这有效地给了我们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
返回一个Base&
,即 Base &d_ref = some_function();
,编译器将不允许进行此类优化。
因此,给定一个对象,编译器知道它的动态类型是Base
,并且Derived&
对派生类型的引用,编译器有权假设它们引用不同的对象。 允许编译器稍微重写代码,假设两个对象不相互引用。 这至少会导致不可预测的行为。 你所做的任何违反优化程序允许做出的假设的行为都是未定义的行为。
- 在函数结束后使用指向变量的指针是否安全?
- char p[0]表示自动分配的缓冲区还是安全指针
- 通过std::shared_ptr使用Rcpp和RcppParallel的线程安全函数指针
- 用非零值初始化void指针的正确(或最安全)方法
- 初始化期间针对安全检查的指针的恒定正确性
- 这C++指针使用线程安全吗?
- 带有此指针的内存安全吗?
- 是否可以访问非线程安全容器内指针指向的值(线程安全映射中的条目)?
- 存储指向 std::string 数据的指针是否安全?
- 为什么引用比指针更安全?
- 为什么在构造函数中将字符串分配给指针是安全的?
- 是否访问指针元组和互斥锁线程安全
- 将数据成员的指针传递给基类构造函数是否安全?
- 将C 中的每个指针删除作为阵列的指针安全吗?
- C++-安全指针范围
- C++中的安全指针取消引用
- 策略模式中应该使用安全指针吗
- c++类用STD安全指针相互链接(c++)
- "安全"指针值间隔?
- 我应该用 C/C++ 重写我的 DSP 例程,还是擅长使用 C# 不安全指针?