隐式转换

来自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 限定符。若泛左值拥有 std::nullptr_t 类型,则作为结果的纯右值是空指针常量 nullptr

除非遇到不求值语境( sizeof 、 typeid 、 noexcept 或 decltype 的运算数中),此转换等效地以原泛左值为构造函数参数,复制构造 T 临时对象,然后将该临时对象作为纯右值返回。

此转换模仿从内存位置读取值到 CPU 寄存器中的行动。

若泛左值所指代的对象含有不确定值(例如由默认初始化非类类型静态变量获得),则行为未定义

除非不确定值拥有可为 cv 限定的无符号字符类型,且不缓存于 CPU 寄存器,或者正式而言:

  • 存储期是静态或线程局域;
  • 或已构造指向它的指针;
  • 或已将它绑定到引用。

若泛左值含有为 delete 所非法化的指针值,则行为亦为实现定义(而非未定义)。

(C++11 起)

[编辑] 数组到指针转换

NT 的数组”或“ 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
  • unsigned charunsigned short 能转换为 int ,若 int 能保有其整个值范围,否则能转换为 unsigned int
  • char 能转换为 intunsigned int ,取决于底层类型: signed charunsigned char (见上述);
  • wchar_tchar16_tchar32_t 能转换为以下列表中能保有其整个值范围的首个类型: intunsigned intlongunsigned longlong longunsigned long long
  • 底层类型不固定的无作用域枚举类型能转换为以下列表中能保有其整个值范围的首个类型: intunsigned intlongunsigned long long 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]或截断有符号数,而零扩展或截断无符号数。
  • 若目标类型有符号,则若源整数能以目标类型表示,则不更改其值。否则结果是实现定义的(注意这异于未定义的有符号整数算术溢出)。
  • 若源类型为 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 相同;
  • 若在 P1 的某级(除了零级)的 cv1
    k
    中有 const ,则在 P2 的同级 cv2
    k
    中有 const
  • 若在 P1 的某级(除了零级)的 cv1
    k
    中有 volatile ,则在 P2 的同级 cv2
    k
    中有 volatile
  • 若在某级 k 上, P2P1更多 cv 限定,则 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

注意 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++14 double * const (*p)[3]double const * const (*p)[3] 的转换非法 转换合法
CWG 616 C++11 任何未初始化对象的左值到右值的转换是未定义行为 允许不定值的 unsigned char
CWG 1423 C++11 std::nullptr_t 在直接或复制初始化中可转换为 bool 只允许直接初始化
CWG 1781 C++11 std::nullptr_tbool 被认为是隐式转换,尽管只对直接初始化合法 不再认为它是隐式转换

[编辑] 参阅

隐式转换C 文档