隐式转换
凡是在语境中使用了某种类型 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; 这样的 explicit 转换函数)。称这种表达式 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 起 OK(两个函数均转换到同一类型 int) switch (i + 0) {} // 始终 OK(隐式转换) }
值变换
值变换是更改表达式值类别的转换。每当将表达式用作期待不同值类别的表达式的运算符的操作数时,发生值变换:
- 对于某个要求纯右值作为它的操作数的运算符,每当泛左值被用作操作数,都会对该表达式应用左值到右值,数组到指针,或者函数到指针 标准转换以将它转换成纯右值。
|
(C++17 起) |
左值到右值转换
任何非函数、非数组类型 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 起) |
整数提升
小整数类型(如 char)和无作用域枚举类型的纯右值可转换成较大整数类型(如 int)的纯右值。具体而言,算术运算符不接受小于 int 的类型作为它的实参,而在左值到右值转换后,如果适用就会自动实施整数提升。此转换始终保持原值。
本段中的以下隐式转换被归类为整数提升。
从整数类型提升
bool 类型的纯右值可转换成 int 类型的纯右值,值 false 变为 0 而 true 变为 1。
对于 bool 以外的整数类型 T
的纯右值 val:
- 在 int 可以表示该位域的所有值的情况下,val 可以转换成 int 类型的纯右值,
- 否则在 unsigned int 可以表示该位域的所有值的情况下,val 可以转换成 unsigned int 类型的纯右值,
- 否则 val 可以按照第 (3) 项中的规则进行转换。
- 如果
T
是 char8_t, (C++20 起)char16_t,char32_t 或 (C++11 起)wchar_t,那么 val 可以按照第 (3) 项中的规则进行转换; - 否则,如果
T
的整数转换等级低于 int 的整数转换等级,那么
- 在 int 可以表示
T
的所有值的情况下,val 可以转换成 int 类型的纯右值, - 否则,val 可以转换成 unsigned int 类型的纯右值。
- 在 int 可以表示
T
是指定的字符类型之一)指定的情况下,val 可转换到以下列表中首个可以表示它的所有值的类型:
- int
- unsigned int
- long
- unsigned long
|
(C++11 起) |
从枚举类型提升
底层类型不固定的无作用域枚举类型可转换到以下列表中首个可以表示它的所有值的类型:
- int
- unsigned int
- long
- unsigned long
|
(C++11 起) |
底层类型固定的无作用域枚举类型可转换到它的底层类型。进而,当底层类型也适用整数提升时,那么也可以转换到提升后的底层类型。对于重载决议,到未提升的底层类型的转换更佳。 |
(C++11 起) |
注意,所有其他转换都不是提升;例如重载决议选择 char -> int(提升)优先于 char -> short(转换)。
浮点提升
float 类型纯右值可转换成 double 类型的纯右值。值不会更改。
该转换被称为浮点提升。
数值转换
不同于提升,数值转换可以更改值,而且有潜在的精度损失。
整数转换
整数类型或无作用域 (C++11 起)枚举类型的纯右值都可隐式转换成任何其他整数类型。如果该转换列在“整数类型提升”下,那么它是提升而非转换。
- 如果目标类型无符号,那么结果值是等于源值模 2n
的最小无符号值,其中 n 用来表示目标类型的位数。
- 即,取决于目标类型更宽或更窄,分别对有符号数进行符号扩展[1]或截断,而对无符号数进行零扩展或截断。
- 如果目标类型有符号,那么当源整数能以目标类型表示时不会更改它的值。否则结果由实现定义 (C++20 前)等于源值模 2n
的唯一目标类型值,其中 n 用于表示目标类型的位数 (C++20 起)(注意这与未定义的有符号整数算术溢出不同)。 - 如果源类型是 bool,那么值 false 转换成目标类型的零,而值 true 转换成目标类型的一(注意如果目标类型是 int,那么这是整数提升,而非整数转换)。
- 如果目标类型是 bool,那么这是布尔转换(见下文)。
浮点转换
浮点类型的纯右值可转换成任意其他浮点类型的纯右值。 |
(C++23 前) |
浮点类型的纯右值可转换成浮点转换等级更高或相等的任意其他浮点类型的纯右值。 标准浮点类型的纯右值可转换成任意其他标准浮点类型的纯右值。 可以使用 |
(C++23 起) |
如果该转换列在“浮点提升”下,那么它是提升而非转换。
- 如果源值能以目标类型精确表示,那么就不会更改它。
- 如果源值处于目标类型的两个可表示值之间,那么结果是这两个值之一(选择哪个由实现定义,不过如果支持 IEEE,那么舍入默认为到最接近)。
- 否则,行为未定义。
浮点整数转换
浮点类型的纯右值可隐式转换成任意整数类型的纯右值。截断小数部分,即舍弃小数部分。
- 如果结果不能适应到目标类型中,那么行为未定义(即使在目标类型是无符号数时,也不会实施模算术)。
- 如果目标类型是 bool,那么这是布尔转换(见下文)。
整数或无作用域枚举类型的纯右值可转换成任意浮点类型的纯右值。结果会尽可能精确。
- 如果该值能适应到目标类型中但不能精确表示,那么选择与之最接近的较高值还是最接近的较低值是由实现定义的,不过如果支持 IEEE,那么舍入默认为到最接近。
- 如果该值不能适应到目标类型中,那么行为未定义。
- 如果源类型是 bool,那么值 false 转换成零,而值 true 转换成一。
指针转换
空指针常量能转换成任何指针类型,而结果是该类型的空指针值。允许这种转换(称为空指针转换)作为单次转换,转换到 cv 限定类型,即不认为它是数值和限定性转换的结合。
指向任何(可有 cv 限定的)对象类型 T
的指针的纯右值,可转换成指向(有相同 cv 限定的)void 的指针的纯右值。结果指针与原指针表示内存中的同一位置。
- 如果原指针是空指针值,那么结果是目标类型的空指针值。
“指向(可有 cv 限定的)Derived
的指针”类型的纯右值 ptr 可以转换成“指向(可有 cv 限定的)Base
的指针”类型的纯右值,其中 Base
是 Derived
的基类,并且 Derived
是完整类类型。如果 Base
不可访问或有歧义,那么程序非良构。
- 如果 ptr 是空指针值,那么结果也是空指针值。
- 否则,如果
Base
是Derived
的虚基类,并且 ptr 没有指向类型与Derived
相似且在自己的生存期内或者正在构造或析构的对象,那么行为未定义。 - 否则,结果是派生类对象的基类子对象。
成员指针转换
空指针常量可转换成任何成员指针类型,而结果是该类型的空成员指针值。允许这种转换(称为空成员指针转换)作为单次转换,转换到 cv 限定类型,即不认为它是数值和限定性转换的结合。
“指向 Base
的(可有 cv 限定的)T
类型成员的指针”类型的纯右值可以转换成“指向 Derived
的(可有 cv 限定的)T
类型成员的指针”,其中 Base
是 Derived
的基类,并且 Derived
是完整类类型。如果 Base
是 Derived
的不可访问、有歧义或虚基类,或是 Derived
的某个中间虚基类的基类,那么程序非良构。
- 如果
Derived
既没有包含该原始成员,也不是包含该原始成员的类的某个基类,那么行为未定义。 - 否则,能以
Derived
对象解引用结果指针,而它将访问该Derived
对象的Base
基类子对象内的成员。
布尔转换
整数、浮点、无作用域枚举、指针和成员指针类型的纯右值,可转换成 bool 类型的纯右值。
零值(对于整数、浮点和无作用域枚举)、空指针值和空成员指针值变为 false。所有其他值变为 true。
直接初始化的语境中,可以 std::nullptr_t 类型的纯右值(包括 nullptr)初始化 bool 对象。结果是 false。然而不认为它是隐式转换。 |
(C++11 起) |
限定性转换
通常来说:
- 指向有 cv 限定的类型
T
的指针类型的纯右值,可转换成指向有更多 cv 限定的同一类型T
的指针纯右值(换言之,能添加常性和易变性)。 - 指向类
X
中有 cv 限定的类型T
的成员指针的纯右值,可转换成指向类X
中有更多 cv 限定的类型T
的成员指针纯右值。
限定性转换的正式定义见下文。
相似类型
非正式地说,忽略顶层 cv 限定性,如果两个类型符合下列条件,那么它们相似:
- 它们是同一类型;或
- 它们都是指针,且被指向的类型相似;或
- 它们都是指向相同类的成员指针,且被指向的成员类型相似;或
- 它们都是数组,且数组元素类型相似。
例如:
- const int* const * 与 int** 相似;
- int (*)(int*) 与 int (*)(const int*) 不相似;
- const int (*)(int*) 与 int (*)(int*) 不相似;
- int (*)(int* const) 与 int (*)(int*) 相似(它们是同一类型);
- std::pair<int, int> 与 std::pair<const int, int> 不相似。
正式地说,类型的相似性基于它们的限定性分解进行定义。
类型 T
的限定性分解 是包含组分 cv_i
和 P_i
的序列,它们对于某些非负 n 可以将 T
分解为 “cv_0 P_0 cv_1 P_1 ... cv_n−1 P_n−1 cv_n U
”,其中:
- 每个
cv_i
都是一个可以包含 const 和 volatile 的集合。 - 每个
P_i
都是以下之一:
- “指向【某类型】的指针”。
- “指向类
C_i
的【某类型】成员的指针”。 - “包含 N_i 个【某类型】元素的数组”。
- “包含【某类型】元素且边界未知的数组”。
如果 P_i
指代数组,那么应用到元素类型的 cv 限定符 cv_i+1
也会作为引用到数组的 cv 限定符 cv_i
。
// T 是 “指向指向 const int 的指针的指针”,它有 3 个限定性分解: // n = 0 -> cv_0 为空,U 是“指向指向 const int 的指针的指针” // n = 1 -> cv_0 为空,P_0 是 “指向【某类型】的指针”, // cv_1 为空,U 是 “指向 const int 的指针” // n = 2 -> cv_0 为空,P_0 是 “指向【某类型】的指针”, // cv_1 为空,P_1 是 “指向【某类型】的指针”, // cv_2 是 “const",U 是 “int” using T = const int**; // 将 U 替换为以下类型之一就可以得到一个限定性分解: // U = U0 -> n = 0 时的限定性分解:U0 // U = U1 -> n = 1 时的限定性分解:指向【U1】的指针 // U = U2 -> n = 2 时的限定性分解:指向【指向【const U2】的指针】的指针 using U2 = int; using U1 = const U2*; using U0 = U1*;
对于类型 T1
和 T2
,如果它们各自有一个限定性分解,使得这两个限定性分解满足以下所有条件,那么这两个类型相似:
- 它们具有相同的 n。
- 它们的
U
指代的类型相同。 - 所有的 i 对应的每对
P_i
组分都各自相同或者一个是“包含 N_i 个【某类型】元素的数组”而另一个是“包含【某类型】元素且边界未知的数组” (C++20 起)。
// n = 2 时的限定性分解: // 指向【指向【const int】的 volatile 指针】的指针 using T1 = const int* volatile *; // n = 2 时的限定性分解: // 指向【指向【int】的指针】的 const 指针 // const pointer to [pointer to [int]] using T2 = int** const; // 以上两个限定性分解的 cv_0、cv_1 和 cv_2 都不同, // 但 n、U、P_0 和 P_1 都相同,因此 T1 和 T2 相似。
合并 cv 限定性
在以下描述中,以 Dn
表示类型 Tn
的最长限定性分解,以 cvn_i
和 Pn_i
表示它的组分。
在满足以下所有条件时,类型是
类型
|
(C++20 前) |
类型
如果类型 |
(C++20 起) |
// T1 的最长限定性分解(n = 2): // 指向【指向【char】的指针】的指针 using T1 = char**; // T2 的最长限定性分解(n = 2): // 指向【指向【const char】的指针】的指针 using T2 = const char**; // 确定 D3 的 cv3_i 和 T_i 组分(n = 2): // cv3_1 = 空(空 cv1_1 和空 cv2_1 的并集) // cv3_2 = “const”(空 cv1_2 和 “const” cv2_2 的并集) // P3_0 = “指向【某类型】的指针”(没有边界未知的数组,因此采用 P1_0) // P3_1 = “指向【某类型】的指针”(没有边界未知的数组,因此采用 P1_1) // cv_2 以外的组分都相同,但 cv3_2 与 cv1_2 不同, // 因此对于 [1, 2) 中的每个 k,都会向 cv3_k 中添加 “const”:cv3_1 会变成 “const”。 // T3 是“指向指向 const char 的 const 指针的指针”,即 const char* const *。 using T3 = /* T1 和 T2 的限定性组合类型 */; int main() { const char c = 'c'; char* pc; T1 ppc = &pc; T2 pcc = ppc; // 错误:T3 与 无 cv 限定的 T2 不同,无法进行限定性转换。 *pcc = &c; *pc = 'C'; // 如果允许上述错误赋值,那么就可以修改 const 对象 “c”, }
注意在 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*(见此处)。
从 C++11 起,显式 bool 转换可以解决安全 bool 问题。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 170 | C++98 | 成员指针转换在派生类不含原始成员时的行为不明确 | 使之明确 |
CWG 172 | C++98 | 枚举类型的提升基于它的底层类型 | 改为基于它的值范围 |
CWG 330 (N4261) |
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 的整数提升方法一致 |
CWG 2485 | C++98 | 涉及位域的整数提升的描述不够好 | 改进描述 |
CWG 2813 | C++23 | 调用类纯右值的显式对象成员函数时会发生临时量实质化 | 不会发生临时量实质化 |
CWG 2861 | C++98 | 指向类型不可访问的对象的指针可以转换成指向基类子对象的指针 | 此时行为未定义 |