![]() |
首页 | 库 | 人员 | 常见问题解答 | 更多 |
如 C++ 对象模型 (§1.7) 所定义,C++ 程序运行的存储或内存是 字节 的连续序列,其中每个字节是位的连续序列。
对象 是存储区 (§1.8) 的一个区域,并具有一个类型 (§3.9)。
类型 是值的离散集合。
类型为 T
的对象具有一个 对象表示,它是在对象中存储的字节序列 (§3.9/4)
类型为 T
的对象具有一个 值表示,它是由确定该类型对象 值 的位集 (§3.9/4)。对于 POD 类型 (§3.9/10),此位集由对象表示给出,但存储中的并非所有位都需要参与值表示(字符类型除外):例如,一些位可能用于填充或可能存在陷阱位。
由对象持有的 类型化值 是由其值表示决定的值。
抽象值(无类型)是表示在类型中的概念信息(即数字 π)。
对象的 固有值 是形成其对象表示的无符号字符序列的二进制值。
抽象 值可以在给定类型中 表示。
在类型 T
中 表示 抽象值 V
是获取与抽象值 V
对应的类型化值 v
。
此操作使用 rep()
运算符表示,如:v=rep(V)
。 v
是 V
在类型 T
中的 表示。
例如,抽象值 π 可以用类型 double
表示为 double value M_PI
,用类型 int
表示为 int value 3
相反,类型化值 可以 抽象。
要 抽象 类型为 T
的类型化值 v
,就是要获取其在 T
中的表示为 v
的抽象值 V
。
此操作使用 abt()
运算符表示,如:V=abt(v)
。
V
是 v
在类型 T
中的 抽象。
抽象只是一个抽象操作(你无法执行它);但它仍然被定义,因为它将用于给出本文档其余部分的定义。
C++ 语言定义了 基本类型 (§3.9.1)。以下基本类型的子集旨在表示 数字
{signed char, signed short int, signed int, signed long int}
可用于表示一般整数(负数和正数)。
{unsigned char, unsigned short int, unsigned int, unsigned long int}
可用于表示具有模运算的正整数。
{float,double,long double}
可用于表示实数。
{{signed integers},{unsigned integers}, bool, char and wchar_t}
{{integer types},{floating types}}
整型要求具有 二进制 值表示。
此外,相同基类型(short
、int
或 long
)的有符号/无符号整型要求具有相同的值表示,即
int i = -3 ; // suppose value representation is: 10011 (sign bit + 4 magnitude bits) unsigned int u = i ; // u is required to have the same 10011 as its value representation.
换句话说,整型有符号/无符号 X 使用相同的值表示,但对其的 解释 不同;即它们的 类型化值 可能不同。
另一个结果是,有符号 X 的范围始终是无符号 X 范围的较小子集,如 §3.9.1/3 所要求。
![]() |
注意 |
---|---|
始终记住,与有符号类型不同,无符号类型具有模运算;也就是说,它们不会溢出。这意味着 - 在混合有符号/无符号类型时要格外小心 - 仅在需要模运算或非常大的数字时使用无符号类型。不要仅仅因为打算只处理正值就使用无符号类型(你也可以使用有符号类型来做到这一点)。 |
本节介绍以下定义,旨在将算术类型与行为类似于数字的用户定义类型集成。一些定义故意很宽泛,以便包括各种各样的用户定义数字类型。
在本库中,术语 数字 指的是抽象数值。
如果类型为 数值,则
如果数值类型表示的抽象值包含负数,则该类型为 有符号。
如果数值类型表示的抽象值不包含负数,则该类型为 无符号。
如果数值类型具有模运算(不会溢出),则该类型为 模运算。
如果数值类型表示的抽象值为整数,则该类型为 整型。
如果数值类型表示的抽象值为实数,则该类型为 浮点型。
算术值 是算术类型的类型化值
数值 是数值类型的类型化值
这些定义只是通过引入一个称为 数值 的超集来概括算术类型和值的标准概念。所有算术类型和值都是数值类型和值,但反之则不然,因为用户定义的数值类型不是算术类型。
以下示例阐明了算术类型和数值类型(以及值)之间的区别
// A numeric type which is not an arithmetic type (is user-defined) // and which is intended to represent integer numbers (i.e., an 'integer' numeric type) class MyInt { MyInt ( long long v ) ; long long to_builtin(); } ; namespace std { template<> numeric_limits<MyInt> { ... } ; } // A 'floating' numeric type (double) which is also an arithmetic type (built-in), // with a float numeric value. double pi = M_PI ; // A 'floating' numeric type with a whole numeric value. // NOTE: numeric values are typed valued, hence, they are, for instance, // integer or floating, despite the value itself being whole or including // a fractional part. double two = 2.0 ; // An integer numeric type with an integer numeric value. MyInt i(1234);
给定一个数字集 N
,它的一些元素可以在数值类型 T
中表示。
类型 T
的可表示值集,或 T
的数值集,是一个数值集,其元素是 N
的某个子集的表示。
例如,int
值的区间 [INT_MIN,INT_MAX]
是 int
类型可表示值的集合,即 int
数字集,对应于抽象值的区间 [abt(INT_MIN),abt(INT_MAX)]
中元素的表示。
类似地,double
值的区间 [-DBL_MAX,DBL_MAX]
是 double
数字集,对应于从 abt(-DBL_MAX)
到 abt(DBL_MAX)
的实数子集。
令 next(x)
表示大于 x 的最小数值。
令 prev(x)
表示小于 x 的最大数值。
令 v=prev(next(V))
和 v=next(prev(V))
是将数值类型的值 v
与数字 V
联系起来的恒等式。
如果数值对 x
,y
满足 x<y
,那么当且仅当 next(x)==y
时,它们是 相邻的。
相邻数值之间的抽象距离通常称为 最后一位单位,简称 ulp。ulp 是一种数量,其抽象大小相对于它所对应的数值:如果数值集不是均匀分布的,也就是说,如果相邻数值之间的抽象距离在集合中变化 - 就像浮点数类型一样 -,那么数值 x
之后的 1ulp 的大小可能(通常是)与数值 y 之后的 1ulp 的大小不同,其中 x!=y
。
由于数字本质上是有序的,因此 T
类型的 数字集 是数值(T
类型)的顺序序列,形式为
REP(T)={l,next(l),next(next(l)),...,prev(prev(h)),prev(h),h}
其中 l
和 h
分别是 T
类型的最小值和最大值,称为 T
类型的边界值。
数字集是离散的。它有一个 大小,即集合中数值的个数,一个 宽度,即最高和最低边界值之间的抽象差值:[abt(h)-abt(l)]
,以及一个 密度,即其大小和宽度之间的关系:density=size/width
。
整数类型具有密度 1,这意味着在 abt(l)
和 abt(h)
之间没有不可表示的整数(即没有间隙)。另一方面,浮点类型具有远小于 1 的密度,这意味着在相邻的浮点值之间存在未表示的实数(即存在间隙)。
抽象值 的区间 [abt(l),abt(h)]
是 T
类型的范围,记为 R(T)
。
范围是一组抽象值,而不是一组数值。在其他文档中,例如 C++ 标准中,词语 range
有时用作 numeric set
的同义词,即从 l
到 h
的数值的有序序列。但是,在本文件中,范围是一个抽象的区间,它包含了数字集。
例如,序列 [-DBL_MAX,DBL_MAX]
是 double
类型的数字集,而实数区间 [abt(-DBL_MAX),abt(DBL_MAX)]
是它的范围。
注意,例如,浮点数类型的范围是 连续的,与它的数字集不同。
选择此定义是因为
此定义允许对 subranged
进行简洁的定义,如上一节所述。
如定义的那样,数字集的宽度与范围的宽度完全相同。
类型的 精度 由数字集的宽度或密度给出。
对于密度为 1 的整数类型,精度在概念上等效于范围,并由值表示中使用的位数决定:位数越多,数字集的大小越大,范围越广,精度越高。
对于密度 <<1 的浮点类型,精度不是由范围的宽度决定的,而是由密度决定的。在典型的实现中,范围由指数中使用的位数决定,精度由尾数中使用的位数决定(给出可以精确表示的最大有效位数)。指数位数越多,范围越广,而尾数位数越多,精度越高。
给定一个抽象值 V
和一个类型 T
以及它对应的范围 [abt(l),abt(h)]
如果 V < abt(l)
或 V > abt(h)
,则 V
在类型 T
中 不可表示(不能表示),或者等效地,它在类型 T
中的表示 超出范围,或者 溢出。
V < abt(l)
,则 溢出为负。V > abt(h)
,则 溢出为正。如果 V >= abt(l)
且 V <= abt(h)
,则 V
在类型 T
中 可表示(可以表示),或者等效地,它在类型 T
中的表示 在范围内,或者 不会溢出。
注意,数值类型,例如 C++ 无符号类型,可以定义任何 V
不会溢出,方法是始终表示的不是 V
本身,而是抽象值 U = [ V % (abt(h)+1) ]
,它始终在范围内。
给定一个在类型 T
中表示为 v
的抽象值 V
,表示的 舍入误差 是抽象差值:(abt(v)-V)
。
注意,表示是一个 操作,因此,舍入误差对应于表示操作,而不是数值本身(即数值本身没有任何误差)
V
在类型 T
中可以精确表示。V
在类型 T
中可以不精确表示。如果类型为 T
的表示 v
-无论它是精确的还是不精确的- 是该类型中 V
的相邻值之一,也就是说,如果 v==prev
或 v==next
,则表示是忠实舍入的。如果 prev
和 next
之间的选择与给定的 舍入方向 相匹配,则它是 正确舍入 的。
所有精确表示都是正确舍入的,但并非所有不精确表示都是。特别是,C++ 要求数值转换(在下面描述)和算术运算的结果(本文件未涵盖)为正确舍入,但批处理操作会传播舍入误差,因此最终结果通常是错误舍入的,也就是说,计算结果的数值 r
既不是抽象值的相邻值之一,而抽象值 R
是理论结果。
因为正确舍入的表示总是被表示的抽象值的相邻值之一,所以舍入误差保证最多为 1ulp。
以下示例总结了给定的定义。考虑
Int
表示整数,具有 数值集: {-2,-1,0,1,2}
和 范围: [-2,2]
Cardinal
表示整数,具有 数值集: {0,1,2,3,4,5,6,7,8,9}
和 范围: [0,9]
(这里没有模运算)Real
表示实数,具有 数值集: {-2.0,-1.5,-1.0,-0.5,-0.0,+0.0,+0.5,+1.0,+1.5,+2.0}
和 范围: [-2.0,+2.0]
Whole
表示实数,具有 数值集: {-2.0,-1.0,0.0,+1.0,+2.0}
和 范围: [-2.0,+2.0]
首先,注意类型 Real
和 Whole
都表示实数,具有相同的范围,但精度不同。
1
(一个抽象值)可以在这些类型中的任何一个中被精确地表示。-1
可以被精确地表示在 Int
、Real
和 Whole
中,但不能表示在 Cardinal
中,会导致负溢出。1.5
可以被精确地表示在 Real
中,并且可以在其他类型中被不精确地表示。1.5
在任何类型(除了 Real
)中被表示为 1
或 2
,则表示是正确舍入的。0.5
在类型 Real
中被表示为 +1.5
,则它是错误舍入的。(-2.0,-1.5)
是任何处于区间 [-2.0,-1.5]
中的实数的 Real
相邻值,但对于 x < -2.0
,也不存在 Real
相邻值,对于 x > +2.0
也不存在。C++ 语言定义了 标准转换 (§4),其中一些是在算术类型之间进行转换。
这些是 整数提升 (§4.5)、整数转换 (§4.7)、浮点提升 (§4.6)、浮点转换 (§4.8) 和 浮点整数转换 (§4.9)。
在下文中,整数提升和浮点提升被称为 算术提升,这些提升以及整数、浮点和浮点整数转换被称为 算术转换(即提升是转换)。
提升,无论是整数还是浮点,都是 值保持 的,这意味着类型值在转换后不会改变。
在下文中,考虑源类型为 S
的源类型值为 s
,源抽象值为 N=abt(s)
,目标类型为 T
;以及在可能的情况下,目标类型值为 t
的类型为 T
。
整数到整数的转换总是定义的
T
是无符号的,则实际表示的抽象值不是 N
,而是 M=[ N % ( abt(h) + 1 ) ]
,其中 h
是类型 T
的最高无符号类型值。T
是有符号的并且 N
不能直接表示,则结果 t
是 实现定义 的,这意味着 C++ 实现需要生成一个值 t
,即使它与 s
完全无关。浮点到浮点的转换仅在 N
可表示时定义;如果不可表示,则转换具有 未定义行为。
N
可以被精确地表示,则 t
必须是精确表示。N
可以被不精确地表示,则 t
必须是两个相邻值之一,并且舍入方向由实现定义;也就是说,转换必须是正确舍入的。浮点到整数的转换不表示 N
,而是表示 M=trunc(N)
,其中 trunc()
是截断:即删除小数部分,如果有的话。
M
不能在 T
中表示,则转换具有 未定义行为(除非 T
是 bool
,参见 §4.12)。整数到浮点的转换总是定义的。
N
可以被精确地表示,则 t
必须是精确表示。N
可以被不精确地表示,则 t
必须是两个相邻值之一,并且舍入方向由实现定义;也就是说,转换必须是正确舍入的。给定一个源类型 S
和一个目标类型 T
,有一个 转换方向,记为:S->T
。
对于任何两个范围,可以定义以下 范围关系:一个范围 X
可以 完全包含 在一个范围 Y
中,在这种情况下,就说 X
被 Y
包含。
形式化:
R(S)
被R(T)
包含,当且仅当(R(S) intersection R(T) ) == R(S)
。
如果源类型范围 R(S)
未包含在目标类型范围 R(T)
中;也就是说,如果 (R(S) & R(T) ) != R(S)
,则称转换方向为 子范围,这意味着 R(S)
未完全包含在 R(T)
中,因此源范围的一部分落在目标范围之外。换句话说,如果转换方向 S->T
是子范围的,则 S
中存在一些值无法在 T
中表示,因为它们超出了范围。请注意,对于 S->T
,子范围形容词适用于 T
。
示例
假设以下所有表示实数的数字类型
X
,数字集为 {-2.0,-1.0,0.0,+1.0,+2.0}
,范围为 [-2.0,+2.0]
Y
,数字集为 {-2.0,-1.5,-1.0,-0.5,0.0,+0.5,+1.0,+1.5,+2.0}
,范围为 [-2.0,+2.0]
Z
,数字集为 {-1.0,0.0,+1.0}
,范围为 [-1.0,+1.0]
对于
R(X) & R(Y) == R(X)
,则 X->Y
不是子范围的。因此,所有类型为 X
的值都可以在类型 Y
中表示。
R(Y) & R(X) == R(Y)
,则 Y->X
不是子范围的。因此,所有类型为 Y
的值都可以在类型 X
中表示,但在这种情况下,一些值是 不精确的 表示的(所有一半的值)。(注意:允许这种情况是因为范围是抽象值的间隔,而不是类型化值的间隔)
R(X) & R(Z) != R(X)
,则 X->Z
是子范围的。因此,一些类型为 X
的值无法在类型 Z
中表示,它们超出了范围 (-2.0 and +2.0)
。
有可能 R(S)
未包含在 R(T)
中,而 R(T)
也不包含在 R(S)
中;例如,UNSIG=[0,255]
未包含在 SIG=[-128,127]
中;SIG
也不包含在 UNSIG
中。这意味着转换方向可能在两种方向上都是子范围的。当涉及到有符号/无符号类型的混合时,就会出现这种情况,这表明在两种方向上都存在可能超出范围的值。
给定转换方向 S->T
的范围关系(子范围或不是子范围),可以将 S
和 T
分别归类为 超类型 和 子类型:如果转换是子范围的,这意味着 T
无法表示所有可能的类型为 S
的值,则 S
是超类型,T
是子类型;否则,T
是超类型,S
是子类型。
例如
R(float)=[-FLT_MAX,FLT_MAX]
和R(double)=[-DBL_MAX,DBL_MAX]
如果 FLT_MAX < DBL_MAX
double->float
是子范围的,并且 supertype=double
,subtype=float
。float->double
不是子范围的,并且 supertype=double
,subtype=float
。请注意,虽然 double->float
是子范围的,float->double
不是,这使得这两个方向具有相同的超类型和子类型。
现在考虑
R(int)=[INT_MIN,INT_MAX]
和R(unsigned int)=[0,UINT_MAX]
C++ 实现需要满足 UINT_MAX > INT_MAX
(§3.9/3),因此
supertype=int
,subtype=unsigned
。supertype=unsigned
,subtype=int
。在这种情况下,转换在两个方向上都是子范围的,并且超类型和子类型对不是不变的(在方向反转下)。这表明两种类型都不能表示彼此的所有值。
当 S->T
和 T->S
的超类型相同时,它实际上表明了一种能够表示子类型所有值的类型。因此,如果转换 X->Y
不是子范围的,而相反方向 (Y->X)
是,这样超类型始终为 Y
,则称方向 X->Y
为 正确舍入值保留,意味着所有此类转换都保证产生范围内的结果,并且正确舍入(即使不精确)。例如,所有整数到浮点数的转换都是正确舍入值保留的。