static_cast 从基本析构函数到派生指针 class 的安全性
Safety of static_cast to pointer-to-derived class from base destructor
这是问题的变体 Downcasting using the Static_cast in C++ and Safety of invalid downcast using static_cast (or reinterpret_cast) for inheritance without added members
关于 ~B 中的行为,我不清楚标准 "B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D" 中的短语。如果在 ~B 中转换为 D,此时它仍然是子对象吗?
下面这个简单的例子说明了问题:
void f(B* b);
class B {
public:
B() {}
~B() { f(this); }
};
class D : public B { public: D() {} };
std::set<D*> ds;
void f(B* b) {
D* d = static_cast<D*>(b); // UB or subobject of type D?
ds.erase(d);
}
我知道 cast 是一扇通向灾难的大门,从 dtor 做这样的事情是个坏主意,但一位同事声称 "The code is valid and works correctly. That cast is perfectly valid. The comment clearly states that it should not be dereferenced"。
我指出强制转换是不必要的,我们应该更喜欢类型系统提供的保护而不是注释。可悲的是,他是 senior/lead 开发人员之一,并且是假定的 c++ "expert".
我可以告诉他演员是 UB 吗?
你应该明确告诉他这是UB! !
为什么?
12.4/7: Bases and members are destroyed in the reverse order of the completion of their constructor The objects are detroyed in the
reverse order of their constuction.
12.6.2/10: First (...) virtual base classes are initialized (...) then, direct base classes are initialized
所以在析构D的时候,先析构D的成员,再析构D的子对象,然后才会析构B。
此代码确保在销毁 B 对象时调用 f()
:
~B() { f(this); }
所以当一个D对象被销毁时,首先销毁D子对象,然后执行~B(),调用f()
。
在 f()
中,您将指向 B 的指针转换为指向 D 的指针。这是 UB:
3.8/5: (...) after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any
pointer that refers to the storage location where the object will be
or was located
may be used but only in limited ways.
(...) The program has undefined behavior if the pointer is used to access a non-static data member or call a non-static member function of the
object, or (...) the pointer is used as the operand of a static_cast.
[expr.static.cast]/p11:
A prvalue of type “pointer to cv1 B
,” where B is a class type, can
be converted to a prvalue of type “pointer to cv2 D
,” where D
is a
class derived (Clause 10) from B
, if a valid standard conversion
from “pointer to D
” to “pointer to B
” exists (4.10), cv2 is the
same cv-qualification as, or greater cv-qualification than, cv1, and
B
is neither a virtual base class of D
nor a base class of a
virtual base class of D
. The null pointer value (4.10) is converted
to the null pointer value of the destination type. If the prvalue of
type “pointer to cv1 B
” points to a B
that is actually a subobject
of an object of type D
, the resulting pointer points to the
enclosing object of type D
. Otherwise, the behavior is undefined.
那么问题是,在 static_cast
时,指针是否实际指向 "a B
that is actually a subobject of an object of type D
"。如果是,则没有UB;如果不是,则行为未定义无论结果指针是否被取消引用或以其他方式使用。
[class.dtor]/p15 说(强调我的)
Once a destructor is invoked for an object, the object no longer
exists
和[basic.life]/p1 表示
The lifetime of an object of type T
ends when:
- if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
- [...]
由此可见,D
对象的生命周期在它的析构函数被调用时就结束了,而且肯定是在 B
的析构函数开始执行时 - 这是在 D
的析构体执行完毕。在这一点上,没有 "object of type D
" 这个 B
可以是它 "no longer exists" 的子对象了。因此,你有UB。
如果 B
是多态的(给定一个虚函数),Clang with UBsan 将 report an error 处理此代码,支持此读取。
很明显,您的同事认为只要您不解除对无效指针的引用就没问题。
他错了。
仅仅评估这样的指针具有未定义的行为。此代码显然已损坏。
这是问题的变体 Downcasting using the Static_cast in C++ and Safety of invalid downcast using static_cast (or reinterpret_cast) for inheritance without added members
关于 ~B 中的行为,我不清楚标准 "B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D" 中的短语。如果在 ~B 中转换为 D,此时它仍然是子对象吗? 下面这个简单的例子说明了问题:
void f(B* b);
class B {
public:
B() {}
~B() { f(this); }
};
class D : public B { public: D() {} };
std::set<D*> ds;
void f(B* b) {
D* d = static_cast<D*>(b); // UB or subobject of type D?
ds.erase(d);
}
我知道 cast 是一扇通向灾难的大门,从 dtor 做这样的事情是个坏主意,但一位同事声称 "The code is valid and works correctly. That cast is perfectly valid. The comment clearly states that it should not be dereferenced"。
我指出强制转换是不必要的,我们应该更喜欢类型系统提供的保护而不是注释。可悲的是,他是 senior/lead 开发人员之一,并且是假定的 c++ "expert".
我可以告诉他演员是 UB 吗?
你应该明确告诉他这是UB! !
为什么?
12.4/7: Bases and members are destroyed in the reverse order of the completion of their constructor The objects are detroyed in the reverse order of their constuction.
12.6.2/10: First (...) virtual base classes are initialized (...) then, direct base classes are initialized
所以在析构D的时候,先析构D的成员,再析构D的子对象,然后才会析构B。
此代码确保在销毁 B 对象时调用 f()
:
~B() { f(this); }
所以当一个D对象被销毁时,首先销毁D子对象,然后执行~B(),调用f()
。
在 f()
中,您将指向 B 的指针转换为指向 D 的指针。这是 UB:
3.8/5: (...) after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. (...) The program has undefined behavior if the pointer is used to access a non-static data member or call a non-static member function of the object, or (...) the pointer is used as the operand of a static_cast.
[expr.static.cast]/p11:
A prvalue of type “pointer to cv1
B
,” where B is a class type, can be converted to a prvalue of type “pointer to cv2D
,” whereD
is a class derived (Clause 10) fromB
, if a valid standard conversion from “pointer toD
” to “pointer toB
” exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, andB
is neither a virtual base class ofD
nor a base class of a virtual base class ofD
. The null pointer value (4.10) is converted to the null pointer value of the destination type. If the prvalue of type “pointer to cv1B
” points to aB
that is actually a subobject of an object of typeD
, the resulting pointer points to the enclosing object of typeD
. Otherwise, the behavior is undefined.
那么问题是,在 static_cast
时,指针是否实际指向 "a B
that is actually a subobject of an object of type D
"。如果是,则没有UB;如果不是,则行为未定义无论结果指针是否被取消引用或以其他方式使用。
[class.dtor]/p15 说(强调我的)
Once a destructor is invoked for an object, the object no longer exists
和[basic.life]/p1 表示
The lifetime of an object of type
T
ends when:
- if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
- [...]
由此可见,D
对象的生命周期在它的析构函数被调用时就结束了,而且肯定是在 B
的析构函数开始执行时 - 这是在 D
的析构体执行完毕。在这一点上,没有 "object of type D
" 这个 B
可以是它 "no longer exists" 的子对象了。因此,你有UB。
如果 B
是多态的(给定一个虚函数),Clang with UBsan 将 report an error 处理此代码,支持此读取。
很明显,您的同事认为只要您不解除对无效指针的引用就没问题。
他错了。
仅仅评估这样的指针具有未定义的行为。此代码显然已损坏。