整数类型间的转换经常会产生意外的结果。例如, 可以拥有比更大范围的值,那么当从 赋值到 并且 的数值超出了 的范围时会发生什么?答案是结果是由实现定义的(比"你不可能明确知道"好听一点的说法)。相同大小整数间的有符号数到无符号数的转换是好的,只要有符号数的数值是正的,但如果有符号数的数值是负的呢?它将被转换为一个大的无符号数,如果这不是你的真实意图,那么就真的是一个问题了。 通过测试范围是否合理来确保转换的有效性,当范围超出时它会抛出异常。
在我们全面认识 之前,我们必须弄清楚支配整数类型的转换及提升的规则。规则有很多并有时很微妙,即使是经验丰富的程序员也会被它们欺骗。与其写出所有这些规则并展开它们,我更愿意给出一些有关转换的例子,它们会引起未定义或令人惊讶的行为,然后再解释所使用的转换规则。
[7]. C++标准在§4.5-4.9中讨论数字类型的提升及转换。
当从一种数字类型赋值给另一种数字类型的变量时,就会发生类型转换。在目标类型可以保存源类型的所有数值的情况下,这种转换是完全安全的,否则就是不安全的。例如, 通常不能保存的最大值,所以当从到的赋值发生时,很大可能的值不能被表示为. 当类型可以表示的数值范围不同时,我们必须确认用于转换的实际数值在目标类型的有效范围之内。否则,我们就会进入实现定义行为的范畴;那就是在把一个超出数字类型可能的数值范围的值赋给这个数字类型时会发生的事情。 实现定义行为意味着具体实现可以自由地做任何它想做的;不同的系统可能有完全不同的行为。 可以确保转换是有效的、合法的,否则就不允许转换。
[8] 无符号数也算,尽管它的行为是有定义的。
用法是一个看起来象C++的转型操作符的函数模板,它泛化了目标类型及源类型。源类型可以从函数的参数隐式推导得到。使用, 要包含头文件。以下两个转换使用 安全地将 转换为 , 以及将 转换为 .
char c=boost::numeric_cast<char>(12);float f=boost::numeric_cast<float>(3.001);
一个最常见的数字转换问题是将来自一个更宽范围的值赋给范围较窄的类型。我们来看看如何帮忙。
从较大的类型到较小类型的赋值从较大的类型(例如)向较小的类型(例如)赋值,有可能数值过大或过小而不能被目标类型所表示。如果这发生了,结果是(是的,正如你猜到的)实现所定义的。我们稍后将讨论无符号类型的潜在问题;我们先从有符号类型开始。C++中有四个内建的有符号类型:
没有人可以绝对肯定哪个类型比其它的大,但典型地,上面的列表是按大小递增的,除了 和 通常具有相同的值范围。但它们都是独立的类型,即使是有相同的大小。想查看你的系统上的类型大小,可以使用 或 和 .
[9] 当然,有符号类型与无符号类型的范围是不同的,即使它们有相同的大小。
当把一个有符号整数类型赋给另一个时,C++标准说:
"若目标类型为有符号类型,在数值可以被目标类型表示时,值不改变;否则,值为实现定义。"
[10] 见C++标准 §4.7.3
以下代码段示范了看起来象是正确的赋值是如何导致实现定义的数值,最后看看如何通过的帮助避免它们。
#include <iostream>#include "boost/cast.hpp"
#include "boost/limits.hpp"
int main() {
std::cout << "larger_to_smaller example\n";
// 没有使用numeric_cast的转换
long l=std::numeric_limits<short>::max();
short s=l;
std::cout << "s is: " << s << '\n';
s=++l;
std::cout << "s is: " << s << "\n\n";
// 使用numeric_cast的转换
try {
l=std::numeric_limits<short>::max();
s=boost::numeric_cast<short>(l);
std::cout << "s is: " << s << '\n';
s=boost::numeric_cast<short>(++l);
std::cout << "s is: " << s << '\n';
}
catch(boost::bad_numeric_cast& e) {
std::cout << e.what() << '\n';
}
}
通过使用 , 被初始化 可以表示的最大值。该值被赋给 并输出。然后, 被加一,这意味着它的值不能再被所表示;它超出了所能表示的范围。把 的新值赋给 , 再次被输出。你可能要问输出的值是什么?好的,因为赋值的结果属于实现定义的行为,这取决于你使用的平台。在我的系统中,使用我的编译器,它变成了一个大的负值,即它被回绕了。必须运行前面的代码才知道在你的系统中会有什么结果。接着,再次执行相同的操作,但这次用了 . 第一个转型成功了,因为数值在范围之内。而第二个转型却会失败,结果是抛出一个 异常。程序的输出如下。
[11] 这种行为和结果在32位平台上十分常见。
larger_to_smaller examples is: 32767
s is: -32768
s is: 32767
bad numeric cast: loss of range in numeric_cast
比避开实现定义行为更为重要的是, 帮助我们避免了错误,否则会很难捕捉到这些错误。那个奇怪的数值可能被传送到应用程序的其它部分,程序可能会继续工作,但几乎可以肯定将产生错误的结果。当然,这仅对于特定的数值会发生这样的情况,如果这些数值很少出现,那么错误将很难被发现。这种错误非常阴险,因为它们仅仅对某些特定值会发生,而不是总会发生。
精宽或取值范围的损失并不常见,如果你不确定一个值对于目标类型是否过大或过小, 就是你可以使用的工具。你甚至可以在不需要的时候使用 ;维护的程序员可能没有象你一样的洞察力。注意,虽然我们在这里只讨论了有符号类型,但同样的原理可应用于于无符号类型。
特殊情况:目标类型为无符号整数无符号整数类型有一个非常有趣的特性,任何数值都有可以合法地赋给它们!对于无符号类型而言,无所谓正或负的溢出。数值被简单地对目标类型最大值加一取模。什么意思?看看以下例子会更清楚一些。
#include <iostream>#include "boost/limits.hpp"
int main() {
unsigned char c;
long l=std::numeric_limits<unsigned char>::max()+14;
c=l;
std::cout << "c is: " << (int)c << '\n';
long reduced=l%(std::numeric_limits<unsigned char>::max()+1);
std::cout << "reduced is: " << reduced << '\n';
}
运行这个程序的输出如下:
c is: 13reduced is: 13
这个例子把一个明显超出可以表示的数值赋给它,然后再计算得到同样的数值。赋值的动作可以用这一行代码来示范:
long reduced=l%(std::numeric_limits<unsigned char>::max()+1);这种行为通常被称为数值回绕(value wrapping)。如果你想用这个特性,就没有必要在这种情况下使用 。此外, 也不接受它。的意图是捕捉错误,而错误应该是因为用户的误解而引起的。如果目标类型不能表示赋给它的数值,就抛出一个 异常。因为无符号整数的算法是明确定义的,不会引起程序员的重大错误。对于 , 重要的是确保获得实际的数值。
[12] 观点是:如果你真的想要数值回绕,就不要使用 .
有符号和无符号整数类型的混用混用有符号和无符号类型可能很有趣,特别是执行算术操作时。普通的赋值也会产生微妙的问题。最常见的问题是将一个负值赋给无符号类型。结果几乎可以肯定不是你原来的意图。另一种情形是从无符号类型到同样大小的有称号类型的赋值。不知什么原因,人们总是会很容易忘记无符号类型可以持有比同样大小的有符号类型更大的值。特别是在表达式或函数调用中更容易忘记。以下例子示范了如何通过来捕捉这种常见的错误。
[13] 当然这是一个高度主观的问题,你的观点可能不同。
#include <iostream>#include "boost/limits.hpp"
#include "boost/cast.hpp"
int main() {
unsigned int ui=std::numeric_limits<unsigned int>::max();
int i;
try {
std::cout << "Assignment from unsigned int to signed int\n";
i=boost::numeric_cast<int>(ui);
}
catch(boost::bad_numeric_cast& e) {
std::cout << e.what() << "\n\n";
}
try {
std::cout << "Assignment from signed int to unsigned int\n";
i=-12;
ui=boost::numeric_cast<unsigned int>(i);
}
catch(boost::bad_numeric_cast& e) {
std::cout << e.what() << "\n\n";
}
}
输出清晰地表明了预期的错误。
Assignment from unsigned int to signed intbad numeric cast: loss of range in numeric_cast
Assignment from signed int to unsigned int
bad numeric cast: loss of range in numeric_cast
基本的规则很简单:无论何时在不同的类型间执行类型转换,都应该使用 来保证转换的安全。
浮点数类型不能帮助我们在浮点数间的转换中避免精度的损失。原因是, , 和 间的转换不象整数类型间的隐式转换那样敏感。记住这点很重要,因为你可能会认为以下代码应该抛出异常。
double d=0.123456789123456;float f=0.123456;
try {
f=boost::numeric_cast<float>(d);
}
catch(boost::bad_numeric_cast& e) {
std::cout << e.what();
}
运行这段代码不会有异常抛出。在许多实现中,从 到 的转换都会导致精度的损失,虽然C++标准没有保证会这样。我们所能知道的就是, 至少具有 的精度。
从浮点数类型转为整数类型又会怎样呢?当一个浮点数类型被转换为一个整数类型,它会被截断;小数部分会被扔掉。 对截断后的数值与目标类型进行相同的检查,就象在两个整数类型间的检查一样。
double d=127.123456789123456;char c;
std::cout << "char type maximum: ";
std::cout << (int)std::numeric_limits<char>::max() << "\n\n";
c=d;
std::cout << "Assignment from double to char: \n";
std::cout << "double: " << d << "\n";
std::cout << "char: " << (int)c << "\n";
std::cout << "Trying the same thing with numeric_cast:\n";
try {
c=boost::numeric_cast<char>(d);
std::cout << "double: " << d;
std::cout << "char: " << (int)c;
}
catch(boost::bad_numeric_cast& e) {
std::cout << e.what();
}
象前面的代码那样进行范围检查以确保有效的赋值是一件令人畏缩的工作。虽然规则看起来很简单,但是有很多组合要被考虑。例如,测试从浮点数到整数的代码看起来就象这样:
template <typename INT, typename FLOAT>bool is_valid_assignment(FLOAT f) {
return std::numeric_limits<INT>::max() >=
static_cast<INT>(f);
}
尽管我已经提起过在一个浮点数类型被转换时,小数部分会被丢弃,在这个实现中还得很容易忽略这个错误。这对于算术类型的转换和提升是自然的。去掉 就可以正确地测试,因为这样 的结果会被转换为浮点数类型。如果是浮点数类型转为整数类型,它会被截断;换句话说,这个函数的问题在于丢失了小数部分。
[14] 这是正常的算术转换结果。
总结提供了算术类型间高效的范围检查转换。在目标类型可以持有所有源类型的值时,使用 没有额外的效率代价。它只在目标类型仅能表示源类型的值的子集时有影响。当转换失败时, 通过抛出一个 异常来表示失败。对于数值类型间的转换有很多复杂的规则,确保转换的正确性是很重要的。
以下情况时使用 :
-
在无符号与有符号类型间进行赋值或比较时
-
在不同大小的整数类型间进行赋值或比较时
-
从一个函数返回类型向一个数值变量赋值,为了预防该函数未来的变化
在这里注意到一个模式了吗?模仿已有的语言和库的名字及行为是简化学习及使用的好方法,但也需要仔细地考虑。增加内建的C++转型就象沿着狭窄的小路行走;一旦迷路会带来很高的代价。遵循语言的语法及语义规则才是负责任的。事实上,对于初学者,内建的转型操作符与看起来象转型操作符的函数可能并没有不同,所以如果行为错误将会导致灾难。 有着与 , , 和 类似的语法和语义。如果它看起来和用起来象转型操作,它就是转型操作,是对转型操作的一个良好的扩展。