隐式转换

来自cppreference.com
< cpp‎ | language
 
 
 
表达式
概述
值类别(左值 lvalue、右值 rvalue、亡值 xvalue)
求值顺序(序列点)
常量表达式
潜在求值表达式
初等表达式
lambda 表达式(C++11)
字面量
整数字面量
浮点字面量
布尔字面量
字符字面量,包含转义序列
字符串字面量
空指针字面量(C++11)
用户定义字面量(C++11)
运算符
赋值运算符a=ba+=ba-=ba*=ba/=ba%=ba&=ba|=ba^=ba<<=ba>>=b
自增与自减++a--aa++a--
算术运算符+a-aa+ba-ba*ba/ba%b~aa&ba|ba^ba<<ba>>b
逻辑运算符a||ba&&b!a
比较运算符a==ba!=ba<ba>ba<=ba>=ba<=>b(C++20)
成员访问运算符a[b]*a&aa->ba.ba->*ba.*b
其他运算符a(...)a,ba?b:c
new 表达式
delete 表达式
throw 表达式
alignof
sizeof
sizeof...(C++11)
typeid
noexcept(C++11)
折叠表达式(C++17)
运算符的代用表示
优先级和结合性
运算符重载
默认比较(C++20)
类型转换
隐式转换
一般算术转换
const_cast
static_cast
reinterpret_cast
dynamic_cast
显式转换 (T)a, T(a)
用户定义转换
 

凡是在语境中使用了某种表达式类型 T1,但语境不接受该类型,而接受另一类型 T2 的时候,会进行隐式转换,具体是:

  • 调用以 T2 为形参声明的函数时,以该表达式作为实参;
  • 运算符期待 T2,而以该表达式作为操作数;
  • 初始化 T2 类型的新对象,包括在返回 T2 的函数中的 return 语句;
  • 将表达式用于 switch 语句(T2 是整数类型);
  • 将表达式用于 if 语句或循环(T2bool)。

仅当存在一个从 T1T2 的无歧义隐式转换序列时,程序良构(能编译)。

如果所调用的函数或运算符存在多个重载,那么将 T1 到每个可用的 T2 都构造隐式转化序列之后,会以重载决议规则决定编译哪个重载。

注意:算术表达式中,针对二元运算符的操作数上的隐式转换的目标类型,是以一组单独的一般算术转换的规则所决定的。

转换顺序

隐式转换序列由下列内容依照这个顺序所构成:

1) 零或一个标准转换序列
2) 零或一个用户定义转换
3) 零或一个标准转换序列

当考虑构造函数或用户定义转换函数的实参时,只允许一个标准转换序列(否则将实际上可以将用户定义转换串连起来)。从一个非类类型转换到另一非类类型时,只允许一个标准转换序列。

标准转换序列由下列内容依照这个顺序所构成:

1) 零或一个来自下列集合者:左值到右值转换数组到指针转换函数到指针转换;
2) 零或一个数值提升数值转换
3) 零或一个函数指针转换
(C++17 起)
4) 零或一个限定转换

用户定义转换由零或一个非显式式单实参转换构造函数或非显式转换函数的调用构成。

当且仅当 T2 能从表达式 e 复制初始化,即对于虚设的临时对象 t,声明 T2 t = e; 良构(能编译)时,称表达式 e 可隐式转换到 T2。注意这与直接初始化T2 t(e))不同,其中还会额外考虑显式构造函数和转换函数。

按语境转换

下列语境中,期待类型 bool,且如果声明 bool t(e); 良构就会进行隐式转换(即考虑如 explicit T::operator bool() const; 这样的隐式转换函数)。称这种表达式 e 按语境转换到 bool

  • ifwhilefor 的控制表达式;
  • 内建逻辑运算符 !&&|| 的操作数;
  • 条件运算符 ?: 的首个操作数;
  • static_assert 声明中的谓词;
  • noexcept 说明符中的表达式;
(C++20 起)
(C++11 起)

期待某个语境特定的类型 T 的下列语境中,只有满足以下条件才能使用具有类类型 E 的表达式 e

(C++14 前)
  • 可允许类型中恰好有一个类型 T,使得 E 拥有非显式转换函数,它的返回类型是(可有 cv 限定的)T 或到(可有 cv 限定的)T 的引用的,且
  • e 可隐式转换到 T
(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

如果程序要求从不完整类型进行左值到右值转换,那么该程序非良构。

当左值到右值转换在 sizeof 的操作数内发生时,不会访问被引用的对象中包含的值,因为该运算符不会对它的操作数求值。

(C++11 前)

当对表达式 E 应用左值到右值转换时,在以下情况下不会访问被引用的对象中包含的值:

(C++11 起)


转换的结果是该左值表示的对象包含的值。

(C++11 前)

转换的结果根据以下规则确定:

  • 如果 T 是(可能有 cv 限定的)std::nullptr_t,那么结果是空指针值。因为转换不会方位该泛左值指代的对象,所以即使在 T 有 volatile 限定的情况下也不会有副作用,并且该泛左值可以指代联合体的非活跃成员。
  • 否则,如果 T 具有类类型,那么:
  • 转换会从该泛左值复制初始化一个具有 T 类型的临时量,并且转换结果是对于此临时量的右值。
(C++17 前)
(C++17 起)
  • 否则,如果该泛左值指代的对象包含的是一个无效指针值,那么行为由实现定义。
  • 否则,结果是该泛左值表示的对象,并且该对象会被读取 (C++20 起)
(C++11 起)

这项转换塑造的是从某个内存位置中读取值到 CPU 寄存器之中的动作。

数组到指针转换

TN 元素数组”或“T 的未知边界数组”类型的左值右值,可隐式转换成“指向 T 的指针”类型的纯右值如果数组是纯右值,那么就会发生临时量实质化 (C++17 起)产生的指针指向数组首元素(细节参阅数组到指针退化)。

临时量实质化

任何完整类型 T纯右值,可转换成同类型 T 的亡值。此转换以该纯右值初始化一个 T 类型的临时对象(以临时对象作为求值该纯右值的结果对象),并产生一个代表该临时对象的亡值。 如果 T 是类类型或类类型的数组,那么它必须有可访问且未被弃置的析构函数:

struct S { int m; };
int k = S().m; // C++17 起成员访问期待泛左值;
               // S() 纯右值被转换成亡值

临时量实质化在下例情况下发生:

注意临时量实质化在从纯右值初始化同类型对象(由直接初始化复制初始化)时出现:直接从初始化器初始化这种对象。这确保“受保证的复制消除”。

(C++17 起)

函数到指针转换

函数类型 T左值,可隐式转换成指向该函数的指针纯右值。这不适用于非静态成员函数,因为不存在指代非静态成员函数的左值。

数值提升

整数提升

小整数类型(如 char)的纯右值可转换成较大整数类型(如 int)的纯右值。具体而言,算术运算符不接受小于 int 的类型作为它的实参,而在左值到右值转换后,如果适用就会自动实施整数提升。此转换始终保持原值。

以下隐式转换被归类为整数提升:

  • boolchar8_t (C++20 起)char16_tchar32_t (C++11 起)wchar_t 以外的,整数转换等级小于 int 的整数类型在 int 可以表示它的所有值时可转换到 int,否则可转换到 unsigned int
  • wchar_tchar8_t (C++20 起)char16_tchar32_t (C++11 起) 可转换到以下列表中可以表示它的所有值的首个类型:
  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long
(C++11 起)

如果以上列表中没有类型能够表示所有值,那么 wchar_tchar8_t (C++20 起)char16_tchar32_t (C++11 起) 可转换到自身的底层类型。

(C++11 起)
  • 底层类型不固定的无作用域枚举类型可转换到以下列表中可以表示它的所有值的首个类型:
  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long
(C++11 起)

如果以上列表中没有类型能够表示所有值,那么该枚举类型可转换到整数转换等级大于 long long 的等级的扩展整数类型中整数转换等级最低的类型。如果这样的类型有两个,那么选择有符号的那个类型。

(C++11 起)
  • 底层类型固定的无作用域枚举类型可转换到它的底层类型,而当底层类型也适用整数提升时,那么也可以转换到提升后的底层类型。到未提升的底层类型的转换优先于重载决议
  • 位域类型在 int 能表示该位域的整个值范围时可转换到 int,否则如果 unsigned int 能表示位域的整个值范围,那么可转换到 unsigned int,否则不实施整数提升;
  • bool 类型可转换到 int,值 false 变为 0true 变为 1

注意,所有其他转换都不是提升;例如重载决议选择 char -> int(提升)优先于 char -> short(转换)。

浮点提升

float 类型纯右值可转换成 double 类型的纯右值。值不更改。

数值转换

不同于提升,数值转换可以更改值,而且有潜在的精度损失。

整数转换

任何整数类型或无作用域 (C++11 起)枚举类型的纯右值都可隐式转换成任何其他整数类型。如果该转换列在“整数类型提升”下,那么它是提升而非转换。

  • 如果目标类型无符号,那么结果值是等于源值 2n
    的最小无符号值,其中 n 用来表示目标类型的位数。
即取决于目标类型更宽或更窄,分别对有符号数进行符号扩展[脚注 1]或截断,而对无符号数进行零扩展或截断。
  • 如果目标类型有符号,那么当源整数能以目标类型表示时不会更改它的值。否则结果由实现定义 (C++20 前)等于源值模 2n
    的唯一目标类型值,其中 n 用于表示目标类型的位数
    (C++20 起)
    (注意这与未定义的有符号整数算术溢出不同)。
  • 如果源类型是 bool,那么值 false 转换成目标类型的零,而值 true 转换成目标类型的一(注意如果目标类型是 int,那么这是整数类型提升,而非整数类型转换)。
  • 如果目标类型是 bool,那么这是布尔转换(见下文)。

浮点转换

浮点类型的纯右值可转换成任何其他浮点类型的纯右值。

(C++23 前)

浮点类型的纯右值可转换成任何其他浮点转换等级更高或相等浮点类型的纯右值。

标准浮点类型的纯右值可转换成任何其他标准浮点类型的纯右值。

可以使用 static_cast 将浮点类型的纯右值显式转换成任何其他浮点类型的纯右值。

(C++23 起)

如果该转换列在“浮点提升”下,那么它是提升而非转换。

  • 如果源值能以目标类型准确表示,那么就不会更改它。
  • 如果源值处于目标类型的两个可表示值之间,那么结果是这两个值之一(选择哪个由实现定义,不过如果支持 IEEE,那么舍入默认为到最接近)。
  • 否则,行为未定义。

浮点整数转换

  • 浮点类型的纯右值可隐式转换成任何整数类型的纯右值。截断小数部分,即舍弃小数部分。如果结果不能适应到目标类型中,那么行为未定义(即使在目标类型是无符号数时,也不会实施模算术)。如果目标类型是 bool,那么这是布尔转换(见下文)。
  • 整数或无作用域 (C++11 起)枚举类型的纯右值可转换成任何浮点类型的纯右值。结果会尽可能精确。如果该值能适应到目标类型中但不能精确表示,那么选择与之最接近的较高值还是最接近的较低值会由实现定义,不过如果支持 IEEE,那么舍入默认为到最接近。如果该值不能适应到目标类型中,那么行为未定义。如果源类型是 bool,那么值 false 转换成零,而值 true 转换成一。

指针转换

  • 空指针常量(见 NULL)能转换成任何指针类型,而结果是该类型的空指针值。允许这种转换(称为空指针转换)作为单次转换,转换到 cv 限定类型,即不认为它是数值和限定性转换的结合。
  • 指向任何(可有 cv 限定的)对象类型 T 的指针的纯右值,可转换成指向(有相同 cv 限定的)void 的指针的纯右值。结果指针与原指针表示内存中的同一位置。如果原指针是空指针值,那么结果是目标类型的空指针值。
  • 指向完整派生类类型的(可有 cv 限定的)空指针可转换成指向它的(有相同 cv 限定的)基类的指针。如果基类不可访问或有歧义,那么转换非良构(不能编译)。转换结果是指向原被指向对象内的基类子对象的指针。空指针值转换成目标类型的空指针值。

成员指针转换

  • 空指针常量(见 NULL)可转换成任何成员指针类型,而结果是该类型的空成员指针值。允许这种转换(称为空成员指针转换)作为单次转换,转换到 cv 限定类型,即不认为它是数值和限定性转换的结合。
  • 指向基类 B 中某类型 T 成员的指针纯右值,可转换成指向它的完整派生类 D 中同一类型 T 成员的指针纯右值。如果 BD 的间接、有歧义或虚基类,或是 D 的某个中间虚基类的基类,那么转换非良构(不能编译)。能以 D 对象解引用结果指针,而它将访问该 D 对象的 B 基类子对象内的成员。空成员指针值转换成目标类型的空成员指针值。

布尔转换

整数、浮点、无作用域 (C++11 起)枚举、指针和成员指针类型的纯右值,可转换成 bool 类型的纯右值。

零值(对于整数、浮点和无作用域 (C++11 起)枚举)、空指针值和空成员指针值变为 false。所有其他值变为 true

直接初始化的语境中,可以 std::nullptr_t 类型纯右值(包括 nullptr)初始化 bool 对象。结果是 false。然而不认为它是隐式转换。

(C++11 起)

限定性转换

  • 指向有 cv 限定的类型 T 的指针类型的纯右值,可转换成指向有更多 cv 限定的同一类型 T 的指针纯右值(换言之,能添加常性和易变性)。
  • 指向类 X 中有 cv 限定的类型 T 的成员指针的纯右值,可转换成指向类 X 中有更多 cv 限定的类型 T 的成员指针纯右值。

“更多” 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
  • 如果在 P1 的某级(除了零级)有未知边界数组类型,那么在 P2 的同级就会有未知边界数组类型;
(C++20 起)
  • 如果在某级 k 上,P2P1更多 cv 限定P1 中有已知边界数组类型而 P2 中有未知边界数组类型 (C++20 起),那么 P2k 为止的每一级(除了零级)cv2
    1
    , cv2
    2
    ... cv2
    k
    上都必须有 const
  • 同样的规则用于指向成员的多级指针及指向对象和指向成员的多级混合指针;
  • 同样的规则适用于包含任何级为指向已知边界或未知边界数组(认为有 cv 限定元素的数组自身有等同的 cv 限定)的多级指针;
(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 起)

脚注

  1. 只有在算术是补码时才会使用,只会对定宽整数类型要求补码。然而注意目前所有拥有 C++ 编译器的平台都使用补码算术。

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 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_tbool 被认为是
隐式转换,尽管只对直接初始化合法
不再认为它是隐式转换
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_tchar16_t 的整数提升
策略不同,但它们都能用这两个策略
char8_tchar16_t
的整数提升方法一致

参阅