隐式转换
当在上下文中使用了某种类型 T1
的表达式,而该上下文不接受该类型,但接受其他类型 T2
时,会执行隐式转换;特别是
- 当表达式用作调用函数的参数时,该函数声明了
T2
作为参数; - 当表达式用作运算符的操作数时,该运算符期望
T2
; - 当初始化
T2
类型的新对象时,包括在返回T2
类型的函数中使用return
语句; - 当表达式用在 switch 语句中时(
T2
是整型); - 当表达式用在 if 语句或循环中时(
T2
是 bool)。
只有当存在从 T1
到 T2
的一个无歧义的隐式转换序列时,程序才是良构的(可编译)。
如果被调用的函数或运算符有多个重载,在从 T1
到每个可用的 T2
构建隐式转换序列后,重载决议规则会决定编译哪个重载。
注意:在算术表达式中,二元运算符操作数的隐式转换目标类型由另一组规则确定:常用算术转换。
目录 |
[编辑] 转换顺序
隐式转换序列按以下顺序组成
当考虑构造函数或用户定义转换函数的参数时,只允许一个标准转换序列(否则用户定义转换可能会有效地链接起来)。当从一个非类类型转换为另一个非类类型时,只允许标准转换序列。
标准转换序列按以下顺序组成
- 左值到右值转换,
- 数组到指针转换,以及
- 函数到指针转换;
3) 零或一个函数指针转换;
|
(自 C++17 起) |
用户定义转换由零或一个非显式的单参数转换构造函数或非显式的转换函数调用组成。
当且仅当 T2
可以从 e 进行复制初始化时,表达式 e 才被认为是隐式可转换为 T2
的,即声明 T2 t = e; 是良构的(可以编译),对于某些发明的临时变量 t
。请注意,这与直接初始化(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
是任何整型或无作用域枚举类型,选择的用户定义转换函数必须是 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) {} // error until C++14 (more than one conversion function) // OK since C++14 (both functions convert to the same type int) switch (i + 0) {} // always okay (implicit conversion) }
[编辑] 值转换
值转换是改变表达式值类别的转换。当表达式作为运算符的操作数出现时,而该运算符期望不同值类别的表达式时,就会发生值转换
- 每当一个泛左值作为运算符的操作数出现,而该运算符需要该操作数为纯右值时,就会应用左值到右值、数组到指针或函数到指针的标准转换,以将表达式转换为纯右值。
|
(自 C++17 起) |
[编辑] 左值到右值转换
左值(在 C++11 之前)泛左值(自 C++11 起) 的任何非函数、非数组类型 T
都可以隐式转换为 右值(在 C++11 之前)纯右值(自 C++11 起)
- 如果
T
不是类类型,则 右值(在 C++11 之前)纯右值(自 C++11 起) 的类型是T
的 cv 非限定版本。 - 否则,右值(在 C++11 之前)纯右值(自 C++11 起) 的类型是
T
。
如果程序需要从不完整类型进行左值到右值转换,则该程序是病态的。
给定泛左值引用的对象为 obj
|
(在 C++11 之前) | ||||
|
(自 C++11 起) |
此转换模拟了将值从内存位置读取到 CPU 寄存器的行为。
[编辑] 数组到指针转换
类型为“T
的 N 元素数组”或“T
的未知边界数组”的左值或右值可以隐式转换为类型为“指向 T
的指针”的纯右值。如果数组是纯右值,则会发生临时对象实质化。(自 C++17 起) 结果指针指向数组的第一个元素(详细信息请参见数组到指针退化)。
[编辑] 函数到指针转换
函数类型的左值可以隐式转换为指向该函数的纯右值指针。这不适用于非静态成员函数,因为不存在引用非静态成员函数的左值。
临时对象实质化任何完整类型 如果 struct S { int m; }; int i = S().m; // member access expects glvalue as of C++17; // S() prvalue is converted to xvalue 临时对象实质化在以下情况下发生
请注意,当从相同类型的纯右值初始化对象时(通过直接初始化或复制初始化),不会发生临时对象实质化:此类对象直接从初始化器初始化。这确保了“保证复制省略”。 |
(自 C++17 起) |
[编辑] 整型提升
小型整型(例如 char)和无作用域枚举类型的纯右值可以转换为较大型整型(例如 int)的纯右值。特别是,算术运算符不接受小于 int 类型的参数,并且如果适用,则在左值到右值转换后自动应用整型提升。此转换始终保留值。
本节中的以下隐式转换被归类为整型提升。
请注意,对于给定的源类型,整型提升的目标类型是唯一的,并且所有其他转换都不是提升。例如,重载决议选择 char -> int(提升)而不是 char -> short(转换)。
[编辑] 从整型类型提升
类型为 prvalue 的 bool 可以转换为类型为 prvalue 的 int,其中 false 变为 0,而 true 变为 1。
对于类型为 T
(除了 bool 之外) 的整型类型的 val 的 prvalue
- 如果 int 可以表示位域的所有值,则 val 可以转换为类型为 prvalue 的 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 可以转换为类型为 prvalue 的 int; - 否则,val 可以转换为类型为 prvalue 的 unsigned int。
- 如果 int 可以表示
T
是给定的字符类型之一)中指定的情况下,val 可以转换为以下类型中第一个可以表示其底层类型所有值的类型的 prvalue- int
- unsigned int
- long
- unsigned long
|
(自 C++11 起) |
[编辑] 从枚举类型提升
底层类型未固定的无作用域 枚举 类型的 prvalue 可以转换为以下列表中第一个能够容纳其整个值范围的类型的 prvalue
- int
- unsigned int
- long
- unsigned long
|
(自 C++11 起) |
底层类型固定的无作用域枚举类型的 prvalue 可以转换为其底层类型。此外,如果底层类型也受整数提升的影响,则转换为提升后的底层类型。为了 重载解析 的目的,转换为未提升的底层类型更好。 |
(自 C++11 起) |
[编辑] 浮点提升
类型为 prvalue 的 prvalue 可以转换为类型为 double 的 prvalue。值不会改变。
此转换称为浮点提升。
[编辑] 数值转换
与提升不同,数值转换可能会更改值,并可能丢失精度。
[编辑] 整型转换
整型类型或无作用域枚举类型的 prvalue 可以转换为任何其他整型类型。如果转换列在整数提升下,则它是提升而不是转换。
- 如果目标类型是无符号类型,则结果值是等于源值的最小无符号值 模 2n
,其中 n 是用于表示目标类型的位数。
- 也就是说,根据目标类型是更宽还是更窄,有符号整数会被符号扩展[1]或截断,而无符号整数分别会被零扩展或截断。
- 如果目标类型是有符号类型,并且源整数可以在目标类型中表示,则值不会改变。否则,结果是 实现定义的(直到 C++20)等于源值模 2n
的目标类型的唯一值,其中 n 是用于表示目标类型的位数(自 C++20 起)(请注意,这与 有符号整数算术溢出 不同,后者是未定义的)。 - 如果源类型是 bool,则值 false 转换为零,值 true 转换为目标类型的值一(请注意,如果目标类型是 int,这是整数提升,而不是整数转换)。
- 如果目标类型是 bool,则这是布尔转换(请参见下文)。
[编辑] 浮点转换
浮点类型的 prvalue 可以转换为任何其他浮点类型的 prvalue。 |
(直到 C++23) |
浮点类型的 prvalue 可以转换为任何其他浮点类型的 prvalue,其浮点转换等级大于或等于。 标准浮点类型的 prvalue 可以转换为任何其他标准浮点类型的 prvalue。
|
(自 C++23 起) |
如果转换列在浮点提升下,则它是提升而不是转换。
- 如果源值可以在目标类型中精确表示,则它不会改变。
- 如果源值介于目标类型的两个可表示值之间,则结果是这两个值之一(具体是哪一个由实现定义,但如果支持 IEEE 算术,则舍入默认到最接近的值)。
- 否则,行为是未定义的。
[编辑] 浮点到整型转换
浮点类型的 prvalue 可以转换为任何整型类型的 prvalue。小数部分被截断,也就是说,小数部分被丢弃。
- 如果截断后的值无法放入目标类型,则行为是未定义的(即使目标类型是无符号类型,模算术也不适用)。
- 如果目标类型是 bool,则这是布尔转换(请参见下文)。
整型或无作用域枚举类型的 prvalue 可以转换为任何浮点类型的 prvalue。如果可能,结果是精确的。
- 如果值可以放入目标类型但无法精确表示,则由实现定义是选择最接近的较高可表示值还是最接近的较低可表示值,但如果支持 IEEE 算术,则舍入默认到最接近的值。
- 如果值无法放入目标类型,则行为是未定义的。
- 如果源类型是 bool,则值 false 转换为零,值 true 转换为一。
[编辑] 指针转换
空指针常量可以转换为任何指针类型,结果是该类型的空指针值。这种转换(称为空指针转换)允许作为单个转换转换为 cv 限定类型,也就是说,它不被视为数值转换和限定转换的组合。
指向任何(可选的 cv 限定)对象类型 T
的 prvalue 指针可以转换为指向(相同 cv 限定的)void 的 prvalue 指针。结果指针表示与原始指针值相同的内存位置。
- 如果原始指针是空指针值,则结果是目标类型的空指针值。
类型为“指向(可能 cv 限定的)Derived
”的 prvalue ptr 可以转换为类型为“指向(可能 cv 限定的)Base
”的 prvalue,其中 Base
是 Derived
的基类,并且 Derived
是完整类类型。如果 Base
是不可访问的或不明确的,则程序是非良构的。
- 如果 ptr 是空指针值,则结果也是空指针值。
- 否则,如果
Base
是Derived
的虚基类,并且 ptr 不指向类型相似于Derived
且在其生命周期内或在其构造或析构期间的对象,则行为是未定义的。 - 否则,结果是指向派生类对象的基类子对象的指针。
[编辑] 指向成员的指针转换
空指针常量可以转换为任何指向成员的指针类型,结果是该类型的空成员指针值。这种转换(称为空成员指针转换)允许作为单个转换转换为 cv 限定类型,也就是说,它不被视为数值转换和限定转换的组合。
类型为“指向类型为(可能 cv 限定的)T
的 Base
的成员的指针”的 prvalue 可以转换为类型为“指向类型为(相同 cv 限定的)T
的 Derived
的成员的指针”的 prvalue,其中 Base
是 Derived
的基类,并且 Derived
是完整类类型。如果 Base
是不可访问的、不明确的或 Derived
的虚基类,或者是 Derived
的某些中间虚基类的基类,则程序是非良构的。
- 如果
Derived
不包含原始成员,并且不是包含原始成员的类的基类,则行为是未定义的。 - 否则,结果指针可以使用
Derived
对象进行解引用,它将访问该Derived
对象的Base
基类子对象内的成员。
[编辑] 布尔转换
整型、浮点型、无作用域枚举、指针和指向成员的指针类型的 prvalue 可以转换为类型为 bool 的 prvalue。
零值(对于整型、浮点型和无作用域枚举)以及空指针和空成员指针值变为 false。所有其他值变为 true。
在 直接初始化 的上下文中,bool 对象可以从类型为 std::nullptr_t 的 prvalue 初始化,包括 nullptr。结果值是 false。但是,这不被认为是隐式转换。 |
(自 C++11 起) |
[编辑] 限定转换
一般来说
- 指向cv 限定类型
T
的指针类型的 prvalue 可以转换为指向更 cv 限定的相同类型T
的 prvalue 指针(换句话说,可以添加常量性和易变性)。 - 类
X
中指向 cv 限定类型T
的成员的指针类型的 prvalue 可以转换为类X
中指向更 cv 限定类型T
的成员的 prvalue 指针。
“限定转换”的形式定义在下文给出。
[编辑] 相似类型
非正式地,如果忽略顶层 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
的序列,使得 T
是 “cv_0 P_0 cv_1 P_1 ... cv_n−1 P_n−1 cv_n U
”,对于非负 n,其中
- 每个
cv_i
都是 const 和 volatile 的集合,并且 - 每个
P_i
是
- “指向...的指针”,
- “指向类
C_i
的类型为...的成员的指针”, - “N_i 的数组”,或
- “未知边界的数组”。
如果 P_i
指定一个数组,则元素类型上的 cv 限定符 cv_i+1
也被视为数组的 cv 限定符 cv_i
。
// T is “pointer to pointer to const int”, it has 3 qualification-decompositions: // n = 0 -> cv_0 is empty, U is “pointer to pointer to const int” // n = 1 -> cv_0 is empty, P_0 is “pointer to”, // cv_1 is empty, U is “pointer to const int” // n = 2 -> cv_0 is empty, P_0 is “pointer to”, // cv_1 is empty, P_1 is “pointer to”, // cv_2 is “const", U is “int” using T = const int**; // substitute any of the following type to U gives one of the decompositions: // U = U0 -> the decomposition with n = 0: U0 // U = U1 -> the decomposition with n = 1: pointer to [U1] // U = U2 -> the decomposition with n = 2: pointer to [pointer to [const U2]] using U2 = int; using U1 = const U2*; using U0 = U1*;
如果它们各自存在一个限定分解,并且两个限定分解满足以下所有条件,则两种类型 T1
和 T2
是相似的
- 它们具有相同的 n。
- 由
U
表示的类型是相同的。 - 对应的
P_i
组件是相同的 或者一个是“N_i 的数组”,另一个是“未知边界的数组”(自 C++20 起),对于所有 i。
// the qualification-decomposition with n = 2: // pointer to [volatile pointer to [const int]] using T1 = const int* volatile *; // the qualification-decomposition with n = 2: // const pointer to [pointer to [int]] using T2 = int** const; // For the two qualification-decompositions above // although cv_0, cv_1 and cv_2 are all different, // they have the same n, U, P_0 and P_1, // therefore types T1 and T2 are similar.
[编辑] 组合 cv 限定符
在下面的描述中,类型 Tn
的最长限定分解表示为 Dn
,其组件表示为 cvn_i
和 Pn_i
。
如果满足以下所有条件,则类型
类型
|
(直到 C++20) |
类型
如果 |
(自 C++20 起) |
// longest qualification-decomposition of T1 (n = 2): // pointer to [pointer to [char]] using T1 = char**; // longest qualification-decomposition of T2 (n = 2): // pointer to [pointer to [const char]] using T2 = const char**; // Determining the cv3_i and T_i components of D3 (n = 2): // cv3_1 = empty (union of empty cv1_1 and empty cv2_1) // cv3_2 = “const” (union of empty cv1_2 and “const” cv2_2) // P3_0 = “pointer to” (no array of unknown bound, use P1_0) // P3_1 = “pointer to” (no array of unknown bound, use P1_1) // All components except cv_2 are the same, cv3_2 is different from cv1_2, // therefore add “const” to cv3_k for each k in [1, 2): cv3_1 becomes “const”. // T3 is “pointer to const pointer to const char”, i.e., const char* const *. using T3 = /* the qualification-combined type of T1 and T2 */; int main() { const char c = 'c'; char* pc; T1 ppc = &pc; T2 pcc = ppc; // Error: T3 is not the same as cv-unqualified T2, // no implicit conversion. *pcc = &c; *pc = 'C'; // If the erroneous assignment above is allowed, // the const object “c” may be modified. }
请注意,在 C 编程语言中,const/volatile 只能添加到第一级
char** p = 0; char * const* p1 = p; // OK in C and C++ const char* const * p2 = p; // error in C, OK in C++
函数指针转换
void (*p)(); void (**pp)() noexcept = &p; // error: cannot convert to pointer to noexcept function struct S { typedef void (*p)(); operator p(); }; void (*q)() noexcept = S(); // error: cannot convert to pointer to noexcept function |
(自 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++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
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 | 一个指向类型不可访问对象的指针可以被 转换为指向基类子对象的指针 |
行为是 在这种情况下是未定义的 |
CWG 2879 | C++17 | 临时对象实体化转换被应用于纯右值 作为期望泛左值的运算符的操作数 |
在某些情况下不应用 |
CWG 2899 | C++98 | 左值到右值的转换可以应用于左值 指定具有无效值表示的对象 |
行为是 在这种情况下是未定义的 |
CWG 2901 | C++98 | 从一个 unsigned int 类型的左值到右值的转换结果 引用一个值为 -1 的 int 对象的左值是不明确的 |
明确化 |
[编辑] 参见
C 文档 关于 隐式转换
|