隐式转换
凡是在语境中使用了某种表达式类型 T1
,但语境不接受该类型,而接受另一类型 T2
的时候,会进行隐式转换,具体是:
- 调用以
T2
为形参声明的函数时,以该表达式为实参; - 运算符期待
T2
,而以该表达式为操作数; - 初始化
T2
类型的新对象,包括在返回T2
的函数中的return
语句; - 将表达式用于
switch
语句(T2
为整型类型); - 将表达式用于
if
语句或循环(T2
为 bool)。
仅当存在一个从 T1
到 T2
的无歧义隐式转换序列时,程序良构(能编译)。
如果所调用的函数或运算符存在多个重载,则将 T1
到每个可用的 T2
都构造隐式转化序列之后,以重载决议规则决定编译哪个重载。
注意:算术表达式中,针对二元运算符的操作数上的隐式转换的目标类型,是以一组单独的通常算术转换的规则所决定的。
转换顺序
隐式转换序列由下列内容依照这个顺序所构成:
当考虑构造函数或用户定义转换函数的实参时,只允许一个标准转换序列(否则将实际上可以将用户定义转换串连起来)。从一个非类类型转换到另一非类类型时,只允许一个标准转换序列。
标准转换序列由下列内容依照这个顺序所构成:
3) 零或一个函数指针转换;
|
(C++17 起) |
用户定义转换由零或一个非 explicit 单实参转换构造函数或非 explicit 转换函数的调用构成。
当且仅当 T2
能从表达式 e
复制初始化,即对于虚设的临时对象 t
,声明 T2 t = e; 良构(能编译)时,称表达式 e
可隐式转换为 T2
。注意这有别于直接初始化(T2 t(e)),其中还会额外考虑 explicit 构造函数和转换函数。
按语境转换
下列语境中,期待类型 bool,且若声明 bool t(e); 良构则进行隐式转换(即考虑如
|
(C++11 起) |
下列语境中,期待某个语境特定的类型 T
,而对于类类型 E
的表达式 e
,仅当 E
拥有单个转换到任何可允许类型的非 explicit 用户定义转换函数 (C++14 前)可允许类型中恰好有一个类型 T
,使得 E
拥有非 explicit 转换函数,其返回类型为(可有 cv 限定的)T
或到(可有 cv 限定的)T
的引用的,且 e
可隐式转换为 T
(C++14 起)时,才得到允许。称这种表达式 e
按语境隐式转换到指定的类型 T
。注意,其中不考虑 explicit 转换函数,虽然在按语境转换到 bool 时会考虑它们。 (C++11 起)
- delete 表达式的实参(
T
是任何对象指针类型); - 整型常量表达式,其中使用了字面类(
T
是任何整数或无作用域枚举类型,所选中的用户定义转换函数必须是 constexpr); -
switch
语句的控制表达式(T
是整数或枚举类型)。
#include <cassert> template<typename T> class zero_init { T val; public: zero_init() : val(static_cast<T>(0)) { } zero_init(T val) : val(val) { } operator T&() { return val; } operator T() const { return val; } }; int main() { zero_init<int> i; assert(i == 0); i = 7; assert(i == 7); switch(i) { } // C++14 前错误(多于一个转换函数) // C++14(两个函数均转换到同一类型 int) switch(i + 0) { } // 始终 OK(隐式转换) }
值变换
值变换是更改表达式值类别的转换。每当将表达式用作期待不同值类别的表达式的运算符的操作数时,发生值变换。
左值到右值转换
任何非函数、非数组类型 T
的泛左值,可隐式转换成同类型的纯右值。若 T
为非类类型,则此转换亦移除 cv 限定符。
以下情况下并不访问该左值所指代的对象:
- 转化发生在不求值语境中,例如作为 sizeof、noexcept、decltype (C++11 起) 或 typeid 的静态形式的操作数
|
(C++11 起) |
- 存储于对象的值是编译时常量,且满足某些其他条件(见 ODR 式使用)
若 T
是非类类型,则产生该对象所包含的值作为纯右值结果。对于类类型,此转换
实际上以原泛左值作为复制构造函数的实参,复制构造一个 |
(C++17 前) |
将泛左值转换为纯右值,其结果对象由该泛左值复制初始化。 |
(C++17 起) |
这项转换所塑造的是从某个内存位置中读取值到 CPU 寄存器之中的动作。
若泛左值所指代的对象含有不确定值(例如由默认初始化非类类型的自动变量而得),则其行为未定义,除非该不确定值的类型为可有 cv 限定的 unsigned char 或 std::byte (C++17 起)。
若泛左值含有已无效化的指针值,则行为由实现定义(而非未定义)。
数组到指针转换
“T
的 N
元素数组”或“T
的未知边界数组”类型的左值或右值,可隐式转换为“指向 T
的指针”类型的纯右值。若数组是纯右值,则发生临时量实质化。 (C++17 起)产生的指针指向数组首元素(细节参阅数组到指针退化)。
临时量实质化任何完整类型 struct S { int m; }; int k = S().m; // C++17 起成员访问期待泛左值; // S() 纯右值被转换为亡值 临时量实质化在下例情况下发生:
注意临时量实质化在从纯右值初始化同类型对象(由直接初始化或复制初始化)时不出现:直接从初始化器初始化这种对象。这确保“受保证的复制消除”。 |
(C++17 起) |
函数到指针
函数类型 T
的左值,可隐式转换成指向该函数的指针的纯右值。这不适用于非静态成员函数,因为不存在指代非静态成员函数的左值。
数值提升
整型提升
小整型类型(如 char)的纯右值可转换为较大整型类型(如 int)的纯右值。具体而言,算术运算符不接受小于 int 的类型为其实参,而在左值到右值转换后,若适用则自动实施整型提升。此转换始终保持原值。
以下隐式转换被归类为整型提升:
-
signed char
或signed short
可转换为 int; - 若 int 能保有其整个值范围,则
unsigned char
、char8_t
(C++20 起) 或unsigned short
可转换为 int,否则可转换为 unsigned int; -
char
可转换为 int 或 unsigned int,取决于其底层类型为 signed char 还是 unsigned char(见上文); -
wchar_t
、char16_t
及char32_t
(C++11 起) 可转换为以下列表中能保有其整个值范围的首个类型:int、unsigned int、long、unsigned long、long long、unsigned long long (C++11 起); - 底层类型不固定的无作用域枚举类型可转换为以下列表中能保有其整个值范围的首个类型:int、unsigned int、long、unsigned long、long long、unsigned long long、扩展整数类型(以大小顺序,有符号优先于无符号) (C++11 起)。若值范围更大,则不应用整型提升;
-
|
(C++11 起) |
- 若 int 能表示位域的整个值范围,则位域类型可转换为 int,否则若 unsigned int 能表示位域的整个值范围,则可转换为 unsigned int,否则不实施整型提升;
- bool 类型可转换为 int,值 false 变为 0 而 true 变为 1。
注意,所有其他转换都不是提升;例如重载决议选择 char -> int(提升)优先于 char -> short(转换)。
浮点提升
float 类型纯右值可转换为 double 类型的纯右值。值不更改。
数值转换
不同于提升,数值转换可以更改值,而且有潜在的精度损失。
整型转换
任何整数类型或无作用域枚举类型的纯右值都可隐式转换成任何其他整数类型。若其转换列出于整数类型提升之下,则它是提升而非转换。
- 若目标类型为无符号,则结果值是等于源值模 2n
的最小无符号值,其中 n 是用于表示目标类型的位数。
- 即取决于目标类型更宽或更窄,分别对有符号数进行符号扩展[脚注 1]或截断,而对无符号数进行零扩展或截断。
- 若目标类型有符号,则当源整数能以目标类型表示时,不更改其值。否则结果是实现定义的 (C++20 前)与源值对 2n
同余的唯一目标类型值,其中 n 是用于表示目标类型的位数 (C++20 起)(注意这不同于未定义的有符号整数算术溢出)。 - 若源类型为 bool,则值 false 转换为目标类型的零,而值 true 转换成目标类型的一(注意若目标类型为 int,则这是整数类型提升,而非整数类型转换)。
- 若目标类型为 bool,则这是布尔转换(见下文)。
- 若目标类型为无符号,则结果值是等于源值模 2n
浮点转换
浮点类型的纯右值可转换成任何其他浮点类型的纯右值。若其转换列于浮点提升之下,则它是提升而非转换。
- 若源值能以目标类型准确表示,则不更改它。
- 若源值处于目标类型的两个可表示值之间,则结果是两个值之一(选择哪个是实现定义的,不过若支持 IEEE,则舍入默认为到最接近)。
- 否则,行为未定义。
浮点整型转换
指针转换
- 空指针常量(见 NULL)能转换成任何指针类型,而结果是该类型的空指针值。允许这种转换(称为空指针转换)作为单次转换,转换到 cv 限定类型,即不认为它是数值和限定性转换的结合。
- 指向任何(可有 cv 限定的)对象类型
T
的指针的纯右值,可转换成指向(有等同 cv 限定的)void 的指针的纯右值。结果指针与原指针表示内存中的同一位置。若原指针是空指针值,则结果为目标类型的空指针值。 - 指向派生类类型的(可有 cv 限定的)空指针可转换成指向其(有等同 cv 限定的)基类的指针。若基类不可访问或有歧义,则转换非良构(不能编译)。转换结果是指向原被指向对象内的基类子对象的指针。空指针值转换成目标类型的空指针值。
成员指针转换
布尔转换
整数、浮点、无作用域枚举、指针和成员指针类型的纯右值,可转换成 bool 类型的纯右值。
零值(对于整数、浮点和无作用域枚举)、空指针值和空成员指针值变为 false。所有其他值变为 true。
直接初始化的语境中,可以 std::nullptr_t 类型纯右值(包括 nullptr)初始化 bool 对象。结果为 false。然而不认为它是隐式转换。 |
(C++11 起) |
限定性转换
“更多” cv 限定表明
- 指向无限定类型的指针能转换成指向
const
的指针; - 指向无限定类型的指针能转换成指向
volatile
的指针; - 指向无限定类型的指针能转换成指向
const volatile
的指针; - 指向
const
类型的指针能转换成指向const volatile
的指针; - 指向
volatile
类型的指针能转换成指向const volatile
的指针。
- 指向无限定类型的指针能转换成指向
对于多级指针,应用下列限制:身为 cv1
0 限定指针,指向 cv1
1 限定指针,指向…… cv1
n-1 限定指针,指向 cv1
n 限定 T
的多级指针 P1
,可转换成身为 cv2
0 限定指针,指向 cv2
1 限定指针,指向…… cv2
n-1 限定指针,指向 cv2
n 限定 T
的多级指针 P2
,仅当
- 两个指针的级数
n
相同;
- 两个指针的级数
|
(C++20 起) |
- 若在
P1
的某级(除了零级)的 cv1
k 中有 const,则在P2
的同级 cv2
k 中有 const; - 若在
P1
的某级(除了零级)的 cv1
k 中有 volatile,则在P2
的同级 cv2
k 中有 volatile;
- 若在
|
(C++20 起) |
- 若在某级
k
上,P2
比P1
有更多 cv 限定或P1
中有已知边界数组类型而P2
中有未知边界数组类型 (C++20 起),则P2
到k
为止的每一级(除了零级)cv2
1, cv2
2 ... cv2
k 上都必须有 const 。 - 同样的规则用于指向成员的多级指针及指向对象和指向成员的多级混合指针;
- 若在某级
|
(C++14 起) |
- 零级由非多级限定性转换的规则处理。
char** p = 0; const char** p1 = p; // 错误:2 级有更多 cv 限定但 1 级非 const const char* const * p2 = p; // OK:2 级有更多 cv 限定并在 1 级添加 const volatile char * const * p3 = p; // OK:2 级更有 cv 限定并在 1 级添加 const volatile const char* const* p4 = p2; // OK:2 级更有 cv 限定而 const 已在 1 级 double *a[2][3]; double const * const (*ap)[3] = a; // C++14 起 OK double * const (*ap1)[] = a; // C++20 起 OK
注意 C 编程语言中,只能添加 const/volatile 到第一级:
char** p = 0; char * const* p1 = p; // C 与 C++ 中 OK const char* const * p2 = p; // C 中错误,C++ 中 OK
函数指针转换
void (*p)(); void (**pp)() noexcept = &p; // 错误:不能转换成指向 noexcept 函数的指针 struct S { typedef void (*p)(); operator p(); }; void (*q)() noexcept = S(); // 错误:不能转换成指向 noexcept 函数的指针 |
(C++17 起) |
安全 bool 问题
在 C++11 引入显式转换函数之前,设计一个能用于布尔语境的类(比如,if(obj) { ... })会出现问题:给定一个用户定义转换函数,如 T::operator bool() const;,则隐式转换序列允许再多一步标准转换序列,也就是 bool 结果会转换成 int,允许诸如 obj << 1; 或 int i = obj; 这样的代码。
一个早期的解决方案可参见 std::basic_ios,它定义 operator! 和 operator void*(C++11 前),使得如 if(std::cin) {...} 的代码能编译,因为 void* 能转换为 bool,但int n = std::cout; 不能,因为 void* 不可转换至 int。这仍然允许无意义代码能编译,如 delete std::cout;。许多 C++11 前的第三方库设计带有更为复杂的解决方案,称作安全 Bool 手法。
显式 bool 转换也能用于解决安全 bool 问题 explicit operator bool() const { ... } |
(C++11 起) |
脚注
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 330 | C++98 | 从 double * const (*p)[3] 到 double const * const (*p)[3] 的转换非法 | 转换合法 |
CWG 616 | C++98 | 任何未初始化对象和拥有非法值的指针对象的左值到右值的转换是未定义行为 | 允许不定值的 unsigned char; 使用非法指针由实现定义 |
CWG 1423 | C++11 | std::nullptr_t 在直接或复制初始化中可转换为 bool
|
只允许直接初始化 |
CWG 1781 | C++11 | std::nullptr_t 到 bool 被认为是隐式转换,尽管只对直接初始化合法
|
不再认为它是隐式转换 |
CWG 1787 | C++98 | 读取缓存在寄存器中的中间 unsigned char 是未定义行为 | 赋予其良好定义 |