3.5.2 防止类型收窄

使用列表初始化还有一个最大优势是可以防止类型收窄(narrowing)。类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。可能导致类型收窄的典型情况如下:

❑从浮点数隐式地转化为整型数。比如:int a=1.2,这里a实际保存的值为整数1,可以视为类型收窄。

❑从高精度的浮点数转为低精度的浮点数,比如从long double隐式地转化为double,或从double转为float。如果这些转换导致精度降低,都可以视为类型收窄。

❑从整型(或者非强类型的枚举)转化为浮点型,如果整型数大到浮点数无法精确地表示,则也可以视为类型收窄。

❑从整型(或者非强类型的枚举)转化为较低长度的整型,比如:unsigned char=1024,1024明显不能被一般长度为8位的unsigned char所容纳,所以也可以视为类型收窄。

值得注意的是,如果变量a从类型A转化为类型B,其值在B中也是可以被表示的,且再转化回类型A能获得原有的值的话,那么这种类型转换也不能叫作类型收窄。所以类型收窄也可以简单地理解为新类型无法表示原有类型数据的值的情况。事实上,发生类型收窄通常也是危险的,应引起程序员的注意。因此,在C++11中,使用初始化列表进行初始化的数据编译器是会检查其是否发生类型收窄的。我们来看看代码清单3-33所示的这个例子。

代码清单3-33


const int x=1024;

const int y=10;

char a=x;//收窄,但可以通过编译

char*b=new char(1024);//收窄,但可以通过编译

char c={x};//收窄,无法通过编译

char d={y};//可以通过编译

unsigned char e{-1};//收窄,无法通过编译

float f{7};//可以通过编译

int g{2.0f};//收窄,无法通过编译

float*h=new float{1e48};//收窄,无法通过编译

float i=1.2l;//可以通过编译

//编译选项:clang++-std=c++11 3-5-5.cpp


在例子代码清单3-33中,我们定义了a到i一共9个需要初始化的变量。可以看到,对于变量a和*b而言,由于其采用的是赋值表达符及圆括号式的表达式初始化,所以虽然它们的数据类型明显收窄(char通常取值范围为-128到127),却不会引发编译失败(事实上,在我们的实验机上会得到编译器的警告)。而使用初始化列表的情况则不一样。对于变量c,由于其类型收窄,则会导致编译器报错。而对于变量d来说,其初始化使用了常量值10,而10是可以由char类型表示的,因此这里不会发生收窄,编译可以通过。同样的情况还发生在变量f、i的初始化上。虽然初始化语句中的变量类型往往“大于”变量声明的类型,但是由于值在f、i中可以表示,还可以被转回原有类型不发生数据改变或者精度错误等,因此也不能算收窄。

比较容易引起疑问的是无符号类型的变量e。虽然按理说e如果再被转换为有符号数,其值依然是-1,但对于无符号数而言,并不能表示-1,因此这里我们也认为e的初始化有收窄的情况。另外,f和g的差别在于2.0f是一个有精度的浮点数值,通常可以认为,将2.0f转换成整型会丢失精度,所以g的声明也是收窄的。

在C++11中,列表初始化是唯一一种可以防止类型收窄的初始化方式。这也是列表初始化区别于其他初始化方式的地方。事实上,现有编译器大多数会在发生类型收窄的时候提示用户,因为类型收窄通常是代码可能出现问题的征兆。C++11将列表初始化设定为可以防范类型收窄,也就是为了加强类型使用的安全性。

总的来说,列表初始化改变了C++中对类型初始化的一些基本模式,将标准程序库跟语言拉得更近了。这样的做法有效地统一了内置类型和自定义类型的行为。这也是C++11设计所遵循的一个思想,即通用为本,专用为末。