隐式转换

来自cppreference.com
< cpp‎ | language

当使用某种类型(比如T1)的表达式,但上下文并不接受该类型而是另一种类型(比如T2)时,发生隐式类型转换,尤其是以下几种情况:

  • 当把表达式作为参数传入函数,而函数参数类型为T2时。
  • 当表达式作为操作数参与运算,而运算符要求T2类型时。
  • 当初始化一个类型为T2的新对象时,包括以return返回函数返回值的情况。
  • 当表达式用于switch语句时(T2是整型)。
  • 当表达式用于if语句,或循环条件时(T2是整型bool)。

该程序是良好的(编译期),仅当存在一个明确的从T1T2隐式转换序列

如果被调用的函数或运算符有多个重载,当T1到每个可用T2的隐式转换序列建立之后,重载决议规则再决定编译哪一个重载。

目录

[编辑] 转换的顺序

隐式转换序列由以下几条组成,依此次序:

1) 零个或一个标准转换序列
2) 零个或一个自定义转换
3) 零个或一个标准转换序列 考虑构造函数或自定义函数的传入参数时,只有一个标准转换序列是允许的(否则自定义转换会循环发生)。当从一个内置类型转换到另一个内置类型,只允许一个标准转换序列。

标准转换序列由以下组成,依此次序:

1) 零个或一个左值变换
2) 零个或一个数值类型提升数值转换
3) 零个或一个函数指针转换;
(since C++17)
4) 零个或一个限定调整

自定义转换包含零个或一个非显式单参数构造函数或者非显式类型转换函数的调用。

表达式e能被称为可隐式转换至T2, 当且仅当T2拷贝构造e, 也就是说T2 t = e;对临时变量t是良好的(能通过编译)。注意要与直接初始化 (T2 t(e))区分开,此时还需考虑显式构造函数和类型转换函数。

[编辑] 上下文转换

下列五种上下文中,要求的是bool类型,且当bool t(e);是良好的时候发生隐式类型转换序列。也就是说,显式的自定义转换函数,比如explicit T::operator bool() const;也在考虑当中。这种表达式e被称为可上下文转换至bool.

  • if, while, for的控制条件;
  • 逻辑运算符 !, &&||;
  • 条件运算符 ?:;
  • static_assert;
  • noexcept.
(since C++11)

以下上下文中,要求上下文特定类型T,类类型Ee表达式可发生转换仅当E存在一个非显式的自定义转换函数将其转换至可接受类型 (until C++14)E有非显式类型转换函数,其类型为(可能有cv限定)T或(可能有cv限定)T类型引用,且e可隐式转换至T (since C++14)时,可接受类型中只有一种类型T。这样的表达式e被称为上下文可隐式转换至特定类型T. 注意,不考虑显式类型转换函数,哪怕在上下文转换至bool时考虑了。 (since 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)
}

[编辑] 左值转换

左值转换发生于使用了左值参数,但要求的是右值的情况。

[编辑] 左值到右值的转换

任一非函数类型,非数组类型的T类型泛左值(glvalue)能隐式转换为同类型的纯右值(prvalue)。如果T不是类,该转换也去除了cv限定符。若该泛左值类型为std::nullptr_t, 则转换后的纯右值是空指针常量nullptr.

除非是不求值上下文(作为sizeof, typeid, noexcept, 或 decltype 的操作数),该转换将原来的泛左值作为构造函数参数,拷贝构造一个临时对象T, 并作为纯右值返回。

该转换模仿把内存地址中的值读入CPU寄存器的行为。

如果该泛左值指代的对象包含不确定值(比如默认初始值的非类的自动变量),该行为未定义

除非该不确定值是可能有cv限定的,不在寄存器内的无符号字符类型,或更正式点地说:

  • 生存期为static或thread-local;
  • 一个已经构造的指向它的指针;
  • 或绑定于它的引用。

当该泛左值为一指针类型,且被delete掉,该行为也取决于实现(而非未定义)。

(since C++11)

[编辑] 数组到指针的转换

一个"长度NT数组",或"长度未定的T数组"的左值或右值能隐式转换为"T类型指针"纯右值。转换后指针指向数组的第一个元素(详见指针到数组的退化)。

[编辑] 函数指针

函数类型T的左值可隐式转换为指向那个函数的纯右值。对非静态成员函数无效,因为指向非静态成员变量的左值不存在。

[编辑] 数值类型提升

[编辑] 整数类型提升

小型整数类型(比如char)的纯右值能隐式转换成更大的整数类型纯右值(比如int)。特别地,算数运算符不接受比int还小的参数类型,如果可行的话类型提升自动在左值到右值转换后发生。这种转换总是不改变原值。

以下类型转换被认为是整数类型提升:

  • signed charsigned short转换到int;
  • unsigned charunsigned short转换到int,仅当它包含全部值域时,否则是unsigned int;
  • char可转换至intunsigned int,取决于它的基础类型: signed charunsigned char(见上);
  • wchar_t, char16_t, 和 char32_t 可转换至下列第一个可包含其全部值域的类型: int, unsigned int, long, unsigned long, long long, unsigned long long;
  • 基础类型未限定的无作用域限定的枚举类型可转换至下列第一个可包含其全部值域的类型: int, unsigned int, long, unsigned long, long long, 或 unsigned long long. 如果超出范围,不发生类型提升。
  • 基础类型限定的无作用域限定的枚举类型可转换为其基础类型的提升类型;
(since C++11)
  • 位域类型可转换为int,仅当包含全部值域,否则unsigned int. 如果仍然超出范围,不发生类型提升;
  • bool可转换至intfalse变成0true变成1.

[编辑] 浮点类型提升

float纯右值可转换为double纯右值。其值不变。

[编辑] 数值类型提升

与上述不同,数值类型提升可能改变值,且可能有精度损失。

[编辑] 整数类型转换

整数类型和无作用域限定的枚举类型的纯右值可转换至任意其他整数类型。如果符合前面整数类型提升的条件,那么就不是整数类型转换。

  • 如果目标类型是无符号型,其结果是与原值 2n
    结果相等的最小无符号值。其中n是用于表示目标类型的位数。
也就是说,取决于目标类型位数更多还是更少,有符号整数会符号扩展[footnote 1]或截断,无符号整数会0-扩展或截断。
  • 如果目标类型是有符号型,当原值可被目标类型表示出来时值不变。否则该值取决于实现。(请与有符号数溢出区分开,它属于未定义行为)。
  • 如果原类型是bool, false转换成0, true转换成目标类型里的某一个值。(如果目标类型是int, 那么是整数类型提升而不是整数类型转换)
  • 如果目标类型是bool, 这属于布尔转换(见下)。

[编辑] 浮点类型转换

浮点类型纯右值可转换成另一浮点类型的纯右值。如果符合前面浮点类型提升的条件,那么就不是浮点类型转换。

  • 当原值可由目标类型精确表示时,值不变。
  • 如果原值介于目标类型的两个有效值之间,转换结果是这两个值之一(究竟是哪一个取决于实现,尽管当支持IEEE算数时会约为最相近的那一个)。
  • 否则,行为未定义。

[编辑] 浮点整数转换

  • 浮点类型纯右值可转换为任一类型整数纯右值。小数部分被截取,也就是舍弃了小数部分。如果该值超出了目标类型范围,行为未定义(哪怕目标类型是无符号型,也不使用模运算)。如果目标类型是bool, 这属于布尔转换(见下)。
  • 整数纯右值和无作用域限定的枚举类型可转换至任一浮点类型。如果该值不能被正确表示,向上还是向下取有效值取决于实现。尽管当支持IEEE算数时会约为最相近值。如果该值超出了目标类型范围,行为未定义。如果原类型是bool, false转换成0, true变成1.

[编辑] 指针转换

  • 空指针常量(见NULL)可转换至任一指针类型,其结果是对应类型的空指针。这种转换(称为空指针转换)允许发生在cv限定的单次转换上,也就是说, 并不认为它是数值转换和限定转换的组合。
  • (可能cv限定的)T类型指针右值可转换为(cv限定情况相同的)void类型指针。转换而成的指针指向原指针指向的内存地址。如果原指针是空指针,那么转换结果是目标类型的空指针。
  • (可能cv限定的)子类指针可转换为它的可访问的, 明确的(cv限定情况相同的)基类指针。转换结果为原指针指向对象的基类子对象。如果原指针是空指针,那么转换结果是目标类型的空指针。

[编辑] 成员指针转换

  • 空指针常量(见NULL)可转换至任一成员指针类型, 其结果是对应类型的空指针。 这种转换(称为空成员指针转换)允许发生在cv限定的单次转换上,也就是说, 并不认为它是数值转换和限定转换的组合。
  • 指向基类B成员T的成员指针纯右值可转换为指向子类D同一成员T的成员指针。如果B不可访问或指代不清,或是D的虚基类或是D的中间虚基类,这种转换为不良的(无法编译)。转换而成的指针可在D内解引用,会操作在D的对应子对象B的对应成员上 如果原指针是空指针,那么转换结果是目标类型的空指针。

[编辑] 布尔转换

整数,浮点数,无作用域限定的枚举类型,指针,成员指针的纯右值能够转换成为bool类型的纯右值。

0(针对整数,浮点数,无作用域限定的枚举类型),空指针,空成员指针能转换为false. 其他所有转换为true.

std::nullptr_t类型纯右值,包括nullptr, 能使用直接初始化变为bool其结果为false. (since C++11)

[编辑] 限定转换

  • cv限定T类型指针纯右值能够转换为更多cv限定的同类型T指针。也就是说,constvolatile能够添加。
  • X内cv限定的T类型成员指针纯右值能够转换为更多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 有const在某一重cv1
    k
    限定中(除开第0重),P2 对应重数也要有constcv2
    k
    中;
  • 如果 P1 有volatile在某一重cv1
    k
    限定中(除开第0重),P2 对应重数也要有volatilecv2
    k
    中;
  • 在某一重k中,P2有比 P1更多的cv限定,那么P2每一重中都要有const(除开第0重)直到k: cv2
    1
    , cv2
    2
    ... cv2
    k
    .
  • 多重成员指针和混合指针同理;
  • 多重指针,包括指向已知长度和未知长度的数组的每一重(cv限定的元素组成的数组,认为其cv限定情况与元素相同);
(since C++14)
  • 0重指针转换已在指针转换中提及。
char** p = 0;
const char** p1 = p; // error: level 2 more cv-qualified but level 1 is not const
const char* const * p2 = p; // OK: level 2 more cv-qualified and const added at level 1
volatile char * const * p3 = p; // OK: level 2 more cv-qual and const added at level 1
volatile const char* const* p4 = p2; // OK: 2 more cv-qual and const was already at 1
 
double *a[2][3];
double const * const (*ap)[3] = a; // OK since C++14

注意在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++

函数指针转换

  • 指向noexcept函数的指针纯右值能转换为指向函数的指针纯右值。
  • 指向noexcept成员函数的指针纯右值能转换为指向成员函数的指针纯右值。
void (*p)() throw(int);
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
(since C++17)

[编辑] bool类安全问题

直到在C++11引入显式转换函数之前,设计一个能用于布尔值上下文的类(比如,if(obj) { ... })会出现一个问题:给出一个自定义转换函数,比如T::operator bool() const;, 隐式转换序列允许再多一步标准转换序列,也就是bool结果会转换成int, 使得以下代码出现,比如 obj << 1; 或者 int i = obj;.

一个早期的解决方案可参见std::basic_ios, 定义了operator!operator void*(until C++11), 使得if(std::cin) {...}能通过编译因为void*能转换为boolint n = std::cout; 不能,因为 void* 不可转换至 int. 这会使得一些乱七八糟的代码也能通过编译,比如 delete std::cout; . 许多C++11之前的第三方库使用了一些更复杂的机制,被称为Safe Bool idiom.

[编辑] 脚注

  1. 仅当使用补码表示时才发生,且仅针对定宽整数类型。请注意,目前所有平台上的C++编译器使用的都是补码运算

[编辑] 缺陷报告

以下行为变化的缺陷报告可追溯至以前发布的C++标准。

报告 适用于 已发布的行为 正确行为
CWG 616 C++11 任何为初始化的左值对象到右值的转换是未定义行为 允许未定值的unsigned char
CWG 1423 C++11 std::nullptr_t 可直接或拷贝构造至bool 只允许直接构造
CWG 330 C++14 double * const (*p)[3] 转换至 double const * const (*p)[3] 无效 有效

[编辑] 另见

隐式转换C文档