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