深入static_cast运算符

来源:互联网 发布:风靡网络 编辑:程序博客网 时间:2024/06/09 16:20

本文下载地址:
http://bbs.sjtu.edu.cn/file/Apple/1200681928238350.pdf

许可:
http://creativecommons.org/licenses/by-nc-nd/2.5/cn

作者:Robert Schmidt
Microsoft Corporation
2000年5月18日

译者:高博
http://www.gaobo.org
feedback@gaobo.org

依我数年在Usenet的C++新闻组的潜水经验来看,四个型别转换运算符:const_cast、dy
namic_cast、reinterpret_cast和static_cast之间究竟关系如何一直是软件工程师的困
扰之源。尤其是reinterpret_cast和static_cast的区别,对许多软件工程师而言十分模
糊——或者不如说简直是视而不见。在以下的两讲里,我会演示一下两者之间的区别,
并给出何时应该应用何者的指南。(译注:本文是讲了static_cast的第一讲。)

概览

顾名思义,static_casts将具有某种静态型别的表达式转换成具有另外某种静态型别的
对象或值。如C++标准(§2.5.9/1-3)(译注:原文误写为5.2.9)所言:

表达式static_cast<T>(v)之结果就是将表达式v转换为型别T的结果。如果T是一个引用
型别,则结果为一个左值。否则,结果是一个右值。在一个static_cast表达式内不可定
义新的型别。static_cast运算符不会将表达式的常量性(constness)转换掉。

……

能够显式地使用形如“static_cast<T>(e)”的static_cast表达式来转换为型别T的表达
式e必须满足:“T t(e);”对某个虚构的临时变量t是一个合式的声明。否则,static_c
ast表达式将执行下面列出的转换之一:

……

其它任何的型别转换都不能够显式地通过使用static_cast来实现。

这就是说,允许使用static_cast的场合包括:

·一个具有目标型别(转换后的型别)的对象能够直接以一个具有源型别(转换前的型
别)的值来初始化,或者

·此种转换符合某个标准§2.5.9中随后列举的例外情形之一(我随后会简要地一一说明
之)。

标准的§2.5.9还提及,static_cast运算符“不会将表达式的常量性转换掉”。标准实
际是说static_cast不会卸除CV-饰词(译注:即常量性——constness,饰词const和挥
发性——volatility,饰词volatile)。比如,static_cast运算符不可以把char
const *转换为char *,尽管它可以把char *转换为char const *。(如果你想要卸除CV
-饰词的话,唯一合用的型别转换运算是const_cast。我会在下面的一个更广义的讨论“
常量正确性”——const correctness的论文里作有关const_cast运算符的完整论述,此
去略去不提。)

例:

现有声明:

struct B{
operator int();
};

struct D : B{
};

B b;

float const f = 0;

下面的型别转换:

static_cast<void *>(&b); // 等价于“(void *) &b;”
static_cast<int>(b); // 等价于“(int) b;”和“int(b);”

都是合式的。第一个型别转换依赖于一个标准支持的从B *到void *的隐式型别转换,而
第二个则隐式调用了b.operator int()。两个型别转换都遵从了标准§2.5.9中描述的一
般规则。但正如我刚才提到的,它还列出了一些一般规则不能涵盖的例外。这些例外使
得以下的转换加入了被允许之列:

static_cast<int>(f); // 等价于“(int) f;”和“int(f);”
static_cast<D &>(b); // 等价于“(D &) b;”

尽管这些转换的目标型别对象是不能用源型别的值来初始化的。

最后,看看这对表达式:

static_cast<int const *>(&f); // 没问题
static_cast<int     *>(&f); // 错误!

第一个转换成功完成了,但第二个可耻地失败了。它们都企图把一个指涉到float的指针
转换成一个指涉到int的指针,但是第二个转换把const饰词也转换掉了——这是标准大
声禁止的。第二个转换如果在老旧的型别转换语法里就能够畅行无阻:

(int *) &f; // 译注:没问题

隐式型别转换

标准的§2.5.9/1允许:

T t = static_cast<T>(e);

当:

T t(e);

是合式的,亦即,直接的、无转型的初始化是可行的。这隐含了一层意思,就是声明:

long l = static_cast<long>('x');



long l('x');

是等价的。你也许会对语言居然允许这样冗余的转型觉得古里古怪,就好像它们仅仅使
源代码显得更加啰嗦,而并没有增加它的兼容性。

本人倒是觉得显式地将所有型别转换以这种形式写出,即使在没有这种必要时也这么做
,反而是有其可圈可点之处的。型别转换是有风险的操作,无论是通过语言规则默许的
,还是显式地通过型别转换运算符或构造函数来进行都是如此。如果你不辞辛劳地把所
有的型别转换都打上标记,你在之后就能够更容易地识别出可能发生问题之处。

有些人建议C++标准委员会加上第五个型别转换运算符——implicit_cast来突显那些语
言无论如何都会允许的型别转换。委员会拒绝了,很有可能是因为以下的模板就是一个
极易写就的等价物:

template<typename FROM, typename TO>
inline TO implicit_cast(FROM const &x){
return x;
}
// 译注:请特别注意这个函数模板的两点,
// 一是它的型别参数是如何被推导出来的,二是它的引数加了一个const饰词,为什么


有了这个模板,我就能把我前面那个例子写成以下形式:

long l = implicit_cast<long>('x');

如果你在显式转换并非必要时觉得写一个static_cast挺别扭的,那可以考虑在你的个人
或项目库里加一个如上的implicit_cast,然后再用(译注:算不算一种心理安慰呢?总
之作者的意思的核心就是为型别转换打上显示的标记)。

显式型别转换

如果static_cast只在能够满足implicit_cast的情形下运作,这个工具就没有存在的必
要了。正如我在早些的例子里演示的那样,static_cast能够使一些语言不允许隐式进行
的型别转换显式进行。这些转换如下:

·任意型别到void
·基础型别到指涉到派生型别的引用
·一些标准型别转换(译注:即implicit_cast允许的转换)的反向转换

任意型别到void

标准上说,任何型别的表达式都可以转换到带CV-饰词(不能卸除但可以添加)的void型
别。这个规则就使下面所有的表达式成为合法:

static_cast<void>(5);
static_cast<const void>("abc");
static_cast<volatile const void>('x');

你可能会想,到底能有什么动机让你把什么东西作一个到void的型别转换,尤其是在你
不能声明型别为void的对象的前提下(译注:声明一个指涉到void的指针是允许的)。
两个可能的理由浮上脑际:

首先,考虑模板:
template<typename R1, typename R2>
R1 adapt(R2 (*f)()){
// ...
return static_cast<R1>(f());
}

adapt接受一个本身无引数、并传回R2型别的函数引数f。adapt调用f(),把f的传回型别
(R2)转换成它自己的传回型别(R1),并传回这个对象。(致好奇之徒:我把这个模
板命名为adapt是因为它的模式和STL中的函数对象配接器如出一辙。)

如果你用下面的函数来具现化这个模板:

int g();
adapt<void>(g);

你又写了这样一个模板特化:
void adapt(int (*f)()){
// ...
return static_cast<void>(f());
}

由于可以转换型别到void,现在你就可以对(译注:传回值为)void和非void的型别使
用相同的模板了(译注:这就是所谓配接器设计模式的强大功能)。

还有一个使用static_cast<void>的动机,就是显式地扬弃表达式的副作用。如果你以:

f();

的形式来调用:

int f();

并且没有用到函数的传回值,维护工程师会猜想是不是你本来要用到这个值的,结果不
小心疏忽了没用。但如果你显式地使用:

static_cast<void>(f());

来抛弃掉这个传回值,那末其它的软件工程师就能够更确信你的意图了。

基础型别到指涉到派生型别的引用
给定型别:

struct Base{
};

struct Derived : Base{
};

以及对象:

Derived derived;
Base &base = derived;

标准允许下面的显式型别转换:

static_cast<Derived &>(base);

标准声称,此转型结果是一个型别为Derived的左值,但我觉得结果是——或者说应该是
——Derived &。

尽管这个转型是合式的,但它还是会带来问题。考虑下面这个相关的例子:

Base base1;
static_cast<Derived &>(base1);

这里,base1实际上就是一个Base型别的对象(译注:它没有一个Derived动态型别)。
这个转换对编译器扯了谎,并将程序引向未定义行为的深渊。

标准还列出了若干种此转换行为的其它限制:
·必须有一个从Derived *到Base *的合法隐式转换。(译注:这意味着什么?什么情况
不存在这种转换?)
·Base不是Derived的一个虚基类(virtual base)
·Derivce至少具有和Base相同的CV-饰词(可以增加但不能卸除)

一些标准型别转换的反向转换

C++标准的第四节列举了一大堆标准的隐式型别转换:

·左值转型到右值
·数组或函数向指针的退化
·增加const和/或volatile饰词
·整数型别和浮点型别之间的转型和型别提升
·指针和指涉到成员的指针之间的转型
·转型到bool

大多数此类型别转换是从C那里继承下来的,所以你应该很熟悉它们才是。由于这些型别
转换是隐式进行的,它们会在没有写任何转型语法的情形下就发生。下面是一些例子:

char a[10];
char *p = a; // 发生了数组型别到指针的退化转型
char const *s = p; // 增加了const饰词
void *v = p; // 指针型别之间的转型
float f = 123; // 整数型别和浮点型别之间的转型
unsigned u = 123;  // 整数型别的型别提升

static_cast允许你开倒车,做法是强制某些转换另辟蹊径。具体地,static_cast让一
些标准转型中整数型别、浮点型别、指针和指涉到成员的指针之间转型和型别提升可以
反向进行,如:

char *p;
void *v;
v = p; // 没问题,隐式转型
p = v; // 错误!不存在这样的隐式转型
p = static_cast<char *>(v); // 没问题,显式转型

显式的static_cast能够高于标准的转型规则,能够让一个本来不能进行的转型运作如仪


static_cast还允许了下面这些反向的型别转换:

·整数型别到枚举型别
·(可带CV-饰词的)Base *到(未卸除CV-饰词的)Derived *
·(可带CV-饰词的)T Base:: *到(未卸除CV-饰词的)T Derived:: *
·(可带CV-饰词的)void *到任意的T *(译注:为什么这里没有“未卸除CV-饰词”的
要求?)

上述转型如果符合下列情形之一,会引发未定义行为:

·整数型别的值不在枚举型别的值的枚举范围之内
·Base *并未指涉到一个Derived对象
·T Base:: *并未指涉到一个Derived成员
·void *并未指涉到一个T对象

请注意static_cast并不是将系统默认型别转换反向运算全部使能的万金油,具体地,它
不能做以下的型别转换:

·右值转型到左值
·指针型别转型到数组型别
·指针型别转型到函数型别
·非bool型别转型到bool型别
·卸除CV-饰词

这些static_cast不能进行的反向型别转换有些是可以用其它的转型形式实现的,而其它
的——比如把一个指针转型到数据型别——是没有任何转型操作能做到的。

向下转型的替代解决方案
很多可以使用static_cast运算符进行的转型操作都和从基础型别到派生型别的转型有关
。我们通常称这样的转型为向下转型,因为它们是沿着继承谱系的向下方向进行的转型
操作。把它和向上转型(从派生型别到基础型别)(译注:泛化)相比,后者通常是以
隐式型别转换的形式所支持的。

向下转型是不安全的,因为它们断言某个具有基础型别为静态型别的实体实际具有一个
派生型别的动态型别。如果你使用static_cast运算符来进行向下转型,哪怕你在睁眼说
瞎话,编译器也会照单全收。假如你真的说了假话,结果就是未定义行为——通常程序
会死得很难看。

尽管我并打算在本讲中讲述有关dynamic_cast运算符的内容,但是我想以简要的方式来
提一下:dynamic_cast运算符是一个在向下转型时static_cast运算符的替代品。dynami
c_cast运算符带来的曙光是它会对你的断言(译注:某个具有基础型别为静态型别的实
体实际具有一个派生型别的动态型别)做一个执行期检查。如果你断言其动态型别为派
生型别的实体根本和派生型别不靠谱,dynamic_cast运算符或者传回一个NULL(对指针
型别而言),或掷出一异常(对引用型别而言)(译注:std::bad_cast型别的异常)。

我非常希望能够在以后的几讲里涵盖dynamic_cast运算符和相关的RTTI(Run-Time
Type Identification,运行时型别识别)主题。

下一讲主题

在下一讲里,我会给出一个对于reinterpret_cast运算符的类似概览,并给出什么时候
应该用reinterpret_cast运算符,什么时候应该用static_cast运算符。

Robert Schmidt is a technical writer for MSDN. His other major writing
distraction is the C/C++ Users Journal, for which he is a contributing
editor and columnist. In previous career incarnations he's been a radio DJ,
wild-animal curator, astronomer, pool-hall operator, private investigator,
newspaper carrier, and college tutor.


☆──────────────────────────────────────☆
   
tstone (追殇&沉淀) 2008年01月19日10:48:19 星期六)
提到:

赞阿
【 在 gaobo 的大作中提到: 】
: 本文下载地址:
: http://bbs.sjtu.edu.cn/file/Apple/1200681928238350.pdf
: 许可:
: http://creativecommons.org/licenses/by-nc-nd/2.5/cn
: 作者:Robert Schmidt
: Microsoft Corporation
: 2000年5月18日
: 译者:高博
: http://www.gaobo.org
: feedback@gaobo.org
: 依我数年在Usenet的C++新闻组的潜水经验来看,四个型别转换运算符:const_cast?.
: namic_cast、reinterpret_cast和static_cast之间究竟关系如何一直是软件工程师?.
: 扰之源。尤其是reinterpret_cast和static_cast的区别,对许多软件工程师而言十?.
: 糊——或者不如说简直是视而不见。在以下的两讲里,我会演示一下两者之间的区别,

: 并给出何时应该应用何者的指南。(译注:本文是讲了static_cast的第一讲。)
: 概览
: 顾名思义,static_casts将具有某种静态型别的表达式转换成具有另外某种静态型别的

: 对象或值。如C++标准(§2.5.9/1-3)(译注:原文误写为5.2.9)所言:
: 表达式static_cast<T>(v)之结果就是将表达式v转换为型别T的结果。如果T是一个引用

: 型别,则结果为一个左值。否则,结果是一个右值。在一个static_cast表达式内不?.
: (以下引言省略...)


☆──────────────────────────────────────☆
   
moonse (moonse) 2008年01月19日12:10:54 星期六)
提到:

一些注解:

static type:
the type of an expression, which results from analysis of the program without
considering execution semantics. The static type of an expression depends only
on the form of the program in which the expression appears, and does not chan
ge while the program is executing.
静态型别:
指不需要考虑表达式的执行期语义,仅从表达式的字面的形式就能够决定的类型。静态类
型在程序运行时不会改变。

dynamic type:
the type of the most derived object to which the lvalue denoted by an lvalue e
xpression refers. The dynamic type of an rvalue expression is its static type.

Example:
If a pointer p whose static type is “pointer to class B” is pointing to an o
bject of class D, derived from B, the dynamic type of the expression *p is “D
.” References are treated similarly.
An object’s dynamic type is determined by the type of the object to which it
currently refers.
动态型别: 由一个左值表达式指出的左值的动态类型,是其所引用的对象的最狭义类型,
由当前所指的对象类型决定。

lvalue & rvalue
An lvalue refers to an object or function.
An rvalue is the result of calling a function that does not return a reference
.
简而言之,左值就是能够出现在赋值符号左面的东西,而右值就是那些可以出现在赋值符
号右面的东西了。左值必须应该是一个变量或者是表达式等,但是它的物理位置是可以确
定的,而右值不一定,这也是它们两者之间的区别。

【 在 gaobo 的大作中提到: 】
: 本文下载地址:
: http://bbs.sjtu.edu.cn/file/Apple/1200681928238350.pdf
: 许可:
: http://creativecommons.org/licenses/by-nc-nd/2.5/cn
: 作者:Robert Schmidt
: Microsoft Corporation
: 2000年5月18日
: 译者:高博
: http://www.gaobo.org
: feedback@gaobo.org
: 依我数年在Usenet的C++新闻组的潜水经验来看,四个型别转换运算符:const_cast?.
: namic_cast、reinterpret_cast和static_cast之间究竟关系如何一直是软件工程师?.
: 扰之源。尤其是reinterpret_cast和static_cast的区别,对许多软件工程师而言十?.
: 糊——或者不如说简直是视而不见。在以下的两讲里,我会演示一下两者之间的区别,

: 并给出何时应该应用何者的指南。(译注:本文是讲了static_cast的第一讲。)
: 概览
: 顾名思义,static_casts将具有某种静态型别的表达式转换成具有另外某种静态型别的

: 对象或值。如C++标准(§2.5.9/1-3)(译注:原文误写为5.2.9)所言:
: 表达式static_cast<T>(v)之结果就是将表达式v转换为型别T的结果。如果T是一个引用

: 型别,则结果为一个左值。否则,结果是一个右值。在一个static_cast表达式内不?.
: (以下引言省略...)


☆──────────────────────────────────────☆
   
gaobo (狄立赫列) 2008年01月19日13:38:15 星期六 提到:

简单地说,所谓左值,就是能够取得一个地址的东西。
某实体只要能用&单目运算符取址,它就是一个左值。
字面常量就不是一个左值,目标型别为非引用类型的转型表达式也不是左值。

要注意的是:
1. 左值不必能够修改,如:const int i = 5; i就是一个左值;
2. 左值也可以是一个表达式,如:

int a, b, c;

// ...

(a>b)?a:b = c;
^^^^^^^^^ 左值

左值可以被指针或引用指涉。

【 在 moonse (moonse) 的大作中提到: 】
: 一些注解:
: static type:
: the type of an expression, which results from analysis of the program without
: considering execution semantics. The static type of an expression depends only
:  on the form of the program in which the expression appears, and does not chan
: ge while the program is executing.
: 静态型别:
: 指不需要考虑表达式的执行期语义,仅从表达式的字面的形式就能够决定的类型。静态类
: 型在程序运行时不会改变。
: dynamic type:
: .................(以下省略)

原创粉丝点击