隐式转换

来自cppreference.com
< cpp‎ | language

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

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

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

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

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

转换顺序

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

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

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

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

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

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

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

按语境转换

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

  • ifwhilefor 的控制表达式;
  • 内建逻辑运算符 !&&|| 的操作数;
  • 条件运算符 ?: 的首个操作数;
  • static_assert 声明中的谓词;
  • noexcept 说明符中的表达式;
(C++20 起)
(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 限定符。

以下情况下并不访问该左值所指代的对象:

  • 转化发生在不求值语境中,例如作为 sizeofnoexceptdecltype (C++11 起)typeid 的静态形式的操作数
  • 泛左值具有 std::nullptr_t 类型:此情况下结果纯右值是空指针常量 nullptr
(C++11 起)
  • 存储于对象的值是编译时常量,且满足某些其他条件(见 ODR 式使用

T 是非类类型,则产生该对象所包含的值作为纯右值结果。对于类类型,此转换

实际上以原泛左值作为复制构造函数的实参,复制构造一个 T 类型的临时对象,并将该临时对象作为纯右值返回。

(C++17 前)

将泛左值转换为纯右值,其结果对象由该泛左值复制初始化。

(C++17 起)

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

若泛左值所指代的对象含有不确定值(例如由默认初始化非类类型的自动变量而得),则其行为未定义,除非该不确定值的类型为可有 cv 限定的 unsigned charstd::byte (C++17 起)

若泛左值含有已无效化的指针值,则行为由实现定义(而非未定义)。

数组到指针转换

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 的类型为其实参,而在左值到右值转换后,若适用则自动实施整型提升。此转换始终保持原值。

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

  • signed charsigned short 可转换为 int
  • int 能保有其整个值范围,则 unsigned charchar8_t (C++20 起)unsigned short 可转换为 int,否则可转换为 unsigned int
  • char 可转换为 intunsigned int,取决于其底层类型为 signed char 还是 unsigned char(见上文);
  • wchar_tchar16_tchar32_t (C++11 起) 可转换为以下列表中能保有其整个值范围的首个类型:intunsigned intlongunsigned longlong longunsigned long long (C++11 起)
  • 底层类型不固定的无作用域枚举类型可转换为以下列表中能保有其整个值范围的首个类型:intunsigned intlongunsigned longlong longunsigned long long、扩展整数类型(以大小顺序,有符号优先于无符号) (C++11 起)。若值范围更大,则不应用整型提升;
  • 底层类型固定的无作用域枚举类型可转换为其底层类型,而当底层类型亦适用整型提升时,则亦可转换为提升后的底层类型。到未提升的底层类型的转换,对于重载决议而言为更优;
(C++11 起)
  • int 能表示位域的整个值范围,则位域类型可转换为 int,否则若 unsigned int 能表示位域的整个值范围,则可转换为 unsigned int,否则不实施整型提升;
  • bool 类型可转换为 int,值 false 变为 0true 变为 1

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

浮点提升

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

数值转换

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

整型转换

任何整数类型或无作用域枚举类型的纯右值都可隐式转换成任何其他整数类型。若其转换列出于整数类型提升之下,则它是提升而非转换。

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

浮点转换

浮点类型的纯右值可转换成任何其他浮点类型的纯右值。若其转换列于浮点提升之下,则它是提升而非转换。

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

浮点整型转换

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

指针转换

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

成员指针转换

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

布尔转换

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

零值(对于整数、浮点和无作用域枚举)、空指针值和空成员指针值变为 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!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 起)

脚注

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

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 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_tbool 被认为是隐式转换,尽管只对直接初始化合法 不再认为它是隐式转换
CWG 1787 C++98 读取缓存在寄存器中的中间 unsigned char 是未定义行为 赋予其良好定义

参阅