static_cast 安全性

14

据我所知,对于指针/引用的static_cast,如果编译器此时无法看到类的定义,则static_cast将像reinterpret_cast一样行事。

static_cast为什么对于数值类型是安全的,而对于指针/引用不安全呢?

2个回答

24
简而言之,是因为多重继承。
详细地说:
#include <iostream>

struct A { int a; };
struct B { int b; };
struct C : A, B { int c; };

int main() {
    C c;
    std::cout << "C is at : " << (void*)(&c) << "\n";
    std::cout << "B is at : " << (void*)static_cast<B*>(&c) << "\n";
    std::cout << "A is at : " << (void*)static_cast<A*>(&c) << "\n";

}

输出:

C is at : 0x22ccd0
B is at : 0x22ccd4
A is at : 0x22ccd0

请注意,为了正确转换为B*,static_cast必须更改指针值。如果编译器没有C类定义,则不会知道B是一个基类,当然也不会知道要应用什么偏移量。
但在没有定义可见的情况下,static_cast不像reinterpret_cast那样行为不受限制。
struct D;
struct E;

int main() {
    E *p1 = 0;
    D *p2 = static_cast<D*>(p1); // doesn't compile
    D *p3 = reinterpret_cast<D*>(p1); // compiles, but isn't very useful
}

一个简单的C风格转换,(B*)(&c)做你说的事情:如果结构体C的定义可见,显示B是基类,则与static_cast相同。如果类型仅前向声明,则与reinterpret_cast相同。这是因为它被设计为与C兼容,这意味着在C中可能的情况下必须执行C所做的操作。

static_cast总是知道如何处理内置类型,这确实是内置类型的含义。它可以将int转换为float等。因此,对于数值类型来说,它始终是安全的,但它不能转换指针,除非(a)它知道它们指向什么,(b)指向类型之间有正确的关系。因此,它可以将int转换为float,但不能将int*转换为float*

正如AndreyT所说,有一种方法可以不安全地使用static_cast,编译器可能无法为您保存,因为代码是合法的:

A a;
C *cp = static_cast<C*>(&a); // compiles, undefined behaviour

其中,static_cast 可以“向下转换”指向派生类(在此情况下,C是A的派生类)的指针。但是,如果被引用的对象实际上并不是派生类,那么你就错了。一个 dynamic_cast 会在运行时执行检查,但是对于我的示例类C,你不能使用 dynamic_cast,因为A没有虚函数。

同样地,你可以使用 static_castvoid* 进行不安全的操作。


多重继承并不是唯一的问题,AndreyT的答案解决了向错误类型进行下转换的问题。 - Patrick Johnmeyer
是的,我只完整回答了问题的第一段,并部分回答了第二段。并没有涉及到标题中的 static_cast 安全性这个广泛问题。 - Steve Jessop

6
不,你的“AFAIK”是不正确的。static_cast从来不像reinterpret_cast那样行为(除非,也许当你转换为void *时,尽管这种转换通常不应由reinterpret_cast执行)。
首先,当static_cast用于指针或引用转换时,static_cast的规范明确要求类型之间存在某种关系,并且该关系已知于static_cast。对于类类型,它们应该被static_cast视为继承关系。如果没有在static_cast的点上完全定义两个类型,则无法满足该要求。因此,如果定义在static_cast的点上不可见,则代码将无法编译。
以下是一些示例来说明上述内容:static_cast可以[多余地]用于执行对象指针向上转换。代码:
Derived *derived = /* whatever */;
Base *base = static_cast<Base *>(derived);

只有当以下代码可编译时,才能编译此代码。
Base *base(derived);

为了编译这个代码,需要同时看到这两种类型的定义。

此外,static_cast 可以用于执行对象指针向下转换。代码如下:

Base *base = /* whatever */;
Derived *derived = static_cast<Derived *>(base);

只有在以下代码可以编译的情况下,才能编译此代码。
Base *base(derived); // reverse direction

再次说明,为了进行编译,这两种类型的定义都必须可见。

因此,您无法使用未定义的类型来使用 static_cast。如果您的编译器允许这样做,那么这是您的编译器中的错误。

static_cast 对于指针/引用可能不安全,原因完全不同。 static_cast 可以对对象指针/引用类型执行分层向下转换而不检查对象的实际动态类型。 static_cast 还可以对方法指针类型执行分层向上转换。如果不小心使用这些未经检查的转换结果,可能会导致未定义的行为。

其次,当使用 static_cast 与算术类型一起使用时,语义完全不同,并且与上述内容无关。它只执行算术类型转换。只要符合您的意图,它们始终是完全安全的(除了范围问题)。事实上,避免在算术转换中使用 static_cast 并改用旧的 C 风格转换可能是一种良好的编程风格,这样可以在源代码中提供清晰的区分,使其区分开始终安全的算术转换和潜在不安全的分层指针/引用转换。


“使用旧的C风格转换而不是” - 这通常也是我所做的,但是然后有人抱怨,我们最终争论是否存在他们认为是邪恶的C风格转换和他们完全满意的单参数构造函数之间的任何区别;-) - Steve Jessop
那么,如果 static_cast 对未定义的指针/引用不进行编译,这种类型的转换就可以被视为安全了吗? - dimba
@idimba:嗯...我很难将“安全”和“不安全”的概念应用于无法编译的代码。我甚至会说这没有多大意义。 - AnT stands with Russia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接