隐式转换和隐式转换序列,基于 C++ 20 ,国庆后简单谈谈。
上下文
id-expression 的值类别
id-espression:一个标识符只有在已被适当声明(良构声明),或者作为 declarator-id 的一部分出现时,才是一个 id-expression。§7.5.4.1/1
- 这里“已被适当声明”是切仅是上一个版本的定义,这显然是不对的,因为声明说明符不是表达式。
- 这里不选择替换而是选择增加,说明 C++ 委员会认为表达式不一定良构。
id-expression 的结果是该标识符所表示的实体。……如果该实体是函数、变量(被命名的对象或引用)、结构化绑定、数据成员或模板参数对象,则该表达式是左值,否则是纯右值。§7.5.4.1/2
/// 下述 id-expression 都是左值。
// 函数
void func();
void (&r)() = func; // `func` : `id-expression`,表示的实体是函数。
// 变量
int x;
int* p = &x; // `x` : `id-expression`,表示的实体是变量。
// 结构化绑定
struct S { int a; };
S s{1};
auto& [x] = s;
int* pa = &x; // `x` : `id-expression`,表示的实体是结构化绑定。
// 数据成员
struct T {
int m;
void f() {
int* pm = &m; // `m` : `id-expression`,表示的实体是数据成员。
}
};
// 模板参数对象
struct Point { int x; int y; };
template<Point origin>
void g() {
const Point *pot = &origin; // `origin` : `id-expression`,表示的实体是模板参数对象。
}
函数调用表达式的值类别
函数调用表达式:§7.6.1.2
- 是左值,如果结果类型是左值引用类型或函数类型的右值引用;
- 是将亡值,如果结果类型是对象类型的右值引用;
- 否则是纯右值。
注:在函数调用表达式的值类别上,非引用情况下都是纯右值。而引用本身具有单独语义,可以使函数调用表达式变成泛左值。
临时物化转换
类型 T 的纯右值可以转换为类型 T 的将亡值。该转换通过以临时对象作为结果对象来求值该纯右值,从而从该纯右值初始化一个类型 T 的临时对象,并产生一个指代该临时对象的将亡值。T 必须是完整类型。§7.3.4/1
struct X { int n; };
X().n;
// `X()` 是纯右值。这里 X 是一个聚合体,X() 是值初始化,总之这个位置是零初始化 -> 纯右值。
// `.n` 要求左侧是泛左值,所以对 `X()` 应用临时物化转换。
// 临时物化转换创建一个类型为 X 的临时对象 t (不可见),用 `X()` 的求值以初始化 `t` 。
// 转换产生一个将亡值 v(不可见),指代 t ,`.n` 消费 v,访问 `t` 的成员 `n` 。
// `.n` 的结果是左值,指代 `t.n` 。
注:
- 在术语上,“临时物化转换”和“左值到右值转换”有严格区别。这种区别在例子数量比较多的时候会很显然,反之则需要强调。
- 虽然他们都有中间的无名对象和表达式,但“临时物化转换”是纯右值到将亡值,“左值到右值转换”是泛左值到纯右值,术语命名有历史原因。
- 临时物化转换更经典的应用在引用绑定上,由于太经典且篇幅太长所以此处不单独展开讲述。
标准注:如果 T 是类类型(或其数组),它必须具有可访问且未被删除的析构函数。(否则无法销毁,是 ill-formed 的)§7.3.4/1
ord
ord:单一定义规则。任何翻译单元不得包含任何可定义项的多个定义。§6.3/1
潜在求值
一个表达式或转换(不一定是可见的)是潜在求值的,除非它是一个未求值的操作数、其(未求值的操作数的)子表达式,或者是在这种语境(未求值语境)下的初始化或转换序列中的转换。
/// >>> 作为未求值操作数的表达式 >>>
// 一个运算符的操作数,如果在运行时不需要被求值,它就是“未求值的操作数”。
// 操作数是未求值操作数的运算符有 sizeof、decltype、noexcept、typeid(非多态类型)。
// sizeof 求一个类型或表达式所占的字节数。它只需要知道操作数的类型,不需要读取值。操作数不求值。
int x = 1;
std::size_t s = sizeof(x); // x 是 sizeof 的操作数,是未求值的操作数。
// decltype 推导一个表达式的类型,不计算表达式的值。操作数不求值。
decltype(x) y = 2; // x 是 decltype 的操作数,是未求值的操作数。
// typeid 获取一个类型或表达式的运行时类型信息。
// 当操作数是非多态类型的泛左值时,操作数不求值;
// 当操作数是多态类型的泛左值时,操作数会被求值(用于运行时类型识别)。
struct S {}; // 非多态类型(无虚函数)
S s;
const std::type_info& ti = typeid(s); // s 是非多态类型,不求值
// noexcept 判断一个表达式是否声明为不抛异常。它不求值操作数,但依赖操作数的形式(调用了哪个函数、该函数是否 noexcept)。
bool b = noexcept(x); // x 是 noexcept 的操作数,是未求值的操作数
/// <<< 作为未求值操作数的表达式 <<<
/// >>> 作为未求值操作数子表达式的表达式 >>>
// sizeof 的操作数不求值,其内部的子表达式也不求值。
int x = 1;
std::size_t s = sizeof(x + 1); // x + 1 是未求值的操作数;x 是它的子表达式,也不求值。
// ……
/// <<< 作为未求值操作数子表达式的表达式 <<<
/// >>> 在未求值语境下的初始化或转换序列中的转换 >>>
// sizeof 中的隐式转换
struct A {
operator int(); // 转换函数:A -> int
};
A a;
std::size_t s = sizeof(a + 0); // a + 0 是未求值的操作数,其中 a 隐式转换为 int 。这个转换不是潜在求值的。
// decltype 中的初始化转换
struct B {
B(int); // 转换构造函数:int -> B
};
decltype(B{1}) b = B{1}; // B{1} 是未求值的操作数,其中 1 被转换为 B 。这个转换不是潜在求值的。
// ……
/// <<< 在未求值语境下的初始化或转换序列中的转换 <<<
详细定义。
表达式 E 的潜在结果集合定义如下:
- 如果 E 是 id-expression,集合只包含 E。
int x; // E = x // x 是 id-expression // 潜在结果集合 = {x} - 如果 E 是带数组操作数的下标操作,集合包含该操作数的潜在结果。
int arr[114514]; // E = arr[0], arr[1], ..., arr[?] 。(E = 带“数组操作数”的下表操作) // 数组操作数 = arr // arr 的潜在结果 = {arr} // 集合 ⊇ {arr} - 如果 E 是形如 E1.template opt E2 的类成员访问表达式,命名一个非静态数据成员,集合包含 E1 的潜在结果。
struct S { int m; }; S s; // E = s.m // E1 = s // . = . // template opt = ε // E2 = m // E1 的潜在结果 = {s} // 集合 ⊇ {s} struct S { template<typename T> int m; /* 非静态数据成员模板。 */ }; S s; int x = s.template m<int>; // E 是 s.template m<int> // E1 = s // . = . // template opt = template // E2 = m // E1 的潜在结果 = {s} // 集合 ⊇ {s} - 如果 E 是命名静态数据成员的类成员访问表达式,集合包含指定该数据成员的 id-expression 。
struct T { static int n; }; // E = T::n // 指定该数据成员的 id-expression = T::n // 集合 ⊇ {T::n} - 如果 E 是形如 E1.* E2 的指针到成员表达式,集合包含 E1 的潜在结果。
struct U { int m; }; U u; int U::* pm = &U::m; // E = u.*pm // E1 = u // E2 = pm // E1 的潜在结果 = {u} // 集合 ⊇ {u} - 如果 E 形如 (E1),集合包含 E1 的潜在结果。
int y; // E = (y) // E1 = y // E1 的潜在结果 = {y} // 集合 ⊇ {y} - 如果 E 是泛左值条件表达式,集合是第二和第三操作数的潜在结果集合的并集。
bool cond; // E = cond ? a : b // 第二操作数 = a // 第三操作数 = b // a 的潜在结果 = {a} // b 的潜在结果 = {b} // 集合 = {a} ∪ {b} - 如果 E 是逗号表达式,集合包含右操作数的潜在结果。
int c, d; // E = (c, d) // 右操作数 = d // d 的潜在结果 = {d} // 集合 ⊇ {d} - 否则,集合为空。
int e = 0; int x = e + 1; // E 是 e + 1 // e + 1 是加法表达式,不属于前面任何一条 // 集合 = {}
§6.3/2
ord-used
如果一个变量 x 的名字出现在一个潜在求值的表达式 E 中,那么 x 被 E 所 odr-used ,除非:
- x 是一个可在常量表达式中使用的引用,或者
constexpr int i = 42; constexpr const int& r = i; // r 是引用,可在常量表达式中使用。 int y = r; // x = r, E = r . // r 出现在潜在求值的表达式 E = r 中,但 r 是常量表达式中使用的引用,故不被 E 所 odr-used 。 - x 是一个非引用类型的变量,可在常量表达式中使用且没有 mutable 子对象,并且 E 是对一个非 volatile 限定的非类类型表达式应用左值到右值转换后,该表达式的潜在结果集合中的一个元素,或者
constexpr int x = 42; // x 是非引用类型变量,可在常量表达式中使用,无 mutable 子对象。 int y = x; // E = x . // x 出现在潜在求值的表达式 E = x 中, // E 是对非 volatile 非类类型 int 应用左值到右值转换,且 x 是 E 的潜在结果集合中的元素(E 是 id-expression,集合只包含 x), // 故 x 不被 E 所 odr 使用。 - x 是一个非引用类型的变量,并且 E 是一个丢弃值表达式的潜在结果集合中的一个元素,且该表达式未应用左值到右值转换。
int x = 42; // x 是非引用类型变量。 x; // E = x,这是一个丢弃值表达式。 // x 出现在潜在求值的表达式 E = x 中,E 是丢弃值表达式 // 该丢弃值表达式未对 x 应用左值到右值转换(因为 x 不是 volatile), // x 是 E 的潜在结果集合中的元素, // 故 x 不被 E 所 odr 使用。
§6.3/4
每个程序应当恰好包含在该程序中在丢弃语句之外被 odr-used 的每个非内联函数或变量的一个定义;不需要诊断(如果违反了该规则,标准不要求编译器警告或报错)。
// 符合条件的情况。
// 翻译单元1
void f();
int main() { f(); } // f 被 odr 使用。
// 翻译单元2
void f() { } // 恰好一个定义。
// 违反“恰好一个”:没有定义。
void g();
int main() { g(); } // g 被 odr 使用,但没有定义。
// 链接错误,但编译器不需要诊断
// 违反“恰好一个”:多个定义。
// 翻译单元1
void h() { }
// 翻译单元2
void h() { } // 重复定义。
// 链接错误,但编译器不需要诊断
// 违反“非内联”:内联函数允许多个定义,不遵循 odr 。
// 翻译单元1
inline void k() { }
// 翻译单元2
inline void k() { } // 允许,只要相同。
// 违反“被 odr 使用”:只声明不使用,不遵循 odr 。
void m(); // 只声明,没有 odr 使用。
// 违反“不在丢弃语句中”:odr-used 在丢弃语句中,不遵循 odr 。
template<bool B>
void n() {
// if constexpr 是 C++17 的编译期 if,条件必须是常量表达式。
// 条件为 false 时,true 分支成为“丢弃语句”(discarded statement)。
// 丢弃语句中的代码不被实例化,不求值。
if constexpr (B) {
// 当 B 为 false 时,这个分支是丢弃语句,而 p() 在丢弃语句中。
// 因为 p() 不在丢弃语句之外被 odr 使用,所以 p 不需要定义。
p();
}
}
§6.3/10
隐式转换
标准转换
标准转换序列的结构
标准转换是具有内置含义(内置含义:由编译器直接实现)的隐式转换,[conv] 中会列举这些隐式转换的集合。标准转换序列是按以下顺序排列的标准转换序列:§7.3/1
- 以下集合中的零个或一个转换:左值到右值转换、数组到指针转换、函数到指针转换。
- 以下集合中的零个或一个转换:整型提升、浮点提升、整型转换、浮点转换、浮点-整型转换、指针转换、指针到成员转换、布尔转换。
- 零个或一个函数指针转换。
- 零个或一个限定转换。
如果需要将表达式转换为所需的目标类型,则将应用标准转换序列。
标准转换是编译器内置的隐式转换。标准转换序列是一个有序的转换链,互斥划分出了四个槽位,每个槽位最多一个转换。这保证源类型到目标类型只有一条转换链。
注:也可以总结出,标准转换序列是按以下顺序排列的标准转换序列:
- 集合“左值变换”的零个或一个元素。
- 集合“提升 \(\cup\) 转换”的零个或一个元素。
- 集合“限定调整”的任意个元素。
发生的语境
具有给定类型(即一个已知的确定的类型)的表达式会在以下几种语境中被隐式转换为其他类型:§7.3/2
- 用作运算符的操作数时;
- 用于 if 或迭代语句的条件时(目标类型是 bool);
- 用于 switch 语句的表达式时(目标类型是整型);
- 用作初始化的源表达式时(包括函数调用实参、return 表达式)。
可行性和效果
表达式 E 可以隐式转换为类型 T,当且仅当对于某个发明的临时变量 t,声明 T t=E; 是良构的。§7.3/3
任何隐式转换的效果等同于执行相应的声明和初始化,然后使用临时变量作为转换的结果。§7.3/6
int x = 1;
int y = x; // 左值 -> 纯右值 -> 左值
// 这里发生了良构的 `int t = x;` 然后 `t` 初始化 `y` ;
// `t` 是无名且临时的纯右值,所指代的对象最终用于初始化 `y` 并被 `y` 指代。
如果 T 是左值引用类型或函数类型的右值引用,结果是左值;如果 T 是对象类型的右值引用,结果是将亡值;否则是纯右值。§7.3/6
/// >>> T 是左值引用类型,或是函数类型的右值引用 >>>
int x = 1;
int& r = x; // T = int&,是左值引用类型。E 的结果是左值。其中 E = x 。
void f();
void (&&rf)() = f; // T = void(&&)(),是函数类型的右值引用。E 的结果是左值。其 E = f 。
/// <<< T 是左值引用类型,或是函数类型的右值引用 <<<
/// >>> T 是对象类型的右值引用 >>>
int x = 1;
int&& rr1 = static_cast<int&&>(x); // T = int&&,是对象类型的右值引用。E 的结果是将亡值。其中 E = = static_cast<int&&>(x) 。
int&& rr2 = std::move(x); // T = int&&,E 结果是将亡值。其中 E = std::move(x) 。
/// <<< T 是对象类型的右值引用 <<<
/// >>> 否则是纯右值 >>>
// 否则 E 是非引用类型。
int x = 1;
int y = x; // T = int,不是引用类型。E 的结果是纯右值。
// 左值 x -> 纯右值 t -> 左值 y。其中 E = t 。
/// <<< 否则是纯右值 <<<
表达式 E 被用作泛左值当且仅当初始化将其用作泛左值。§7.3/6
- 这是对上面的可行性和效果的补充说明。例如:
int x = 1;
int y = x; // T = int,不是引用类型。E 的结果是纯右值。
// 纯右值 E 不应该是左值 x 。
// 考虑:左值 x -> 隐式的纯右值 t -> 左值 y ,E 应是 t 。
被抑制的语境
在某些语境中,特定转换会被抑制。例如,一元 & 运算符的操作数上不会进行左值到右值转换。具体例外在那些运算符和语境的描述中给出。§7.3/8
- 这里讲的是不遵循上述规则的场景,是额外的特例。具体特例和运算符相关,如下使用了一个一般性的例子。
int x = 1;
int* p = &x; // x 不进行左值到右值转换,取的是 x 的地址。
左值转换
左值到右值转换
左值到右值转换的规则:§7.3.1/1
- 非函数、非数组类型 T 的泛左值可以转换为纯右值。
- 如果 T 是不完整类型,需要此转换的程序是 ill-formed 的。
- 如果 T 是非类类型,纯右值的类型是 T 的 cv 未限定版本。
- 否则,纯右值的类型是 T。
/// >>> 1. 非函数、非数组类型 T :限定范围 >>>
int x = 1; // T = int ,非函数、非数组类型。
int y = x; // 泛左值 x -> 纯右值 t -> 泛左值 y 。
/// >>> 2. 不完整类型 T :良构条件 >>>
// 前置声明,S 的定义在其他翻译单元,是不完整类型。
struct S;
S v = getS();
// getS() 的声明由某个头文件提供,声明形式为:S& getS();
// 返回左值引用的函数调用表达式 E 是左值,E 初始化 v 需要左值到右值转换。
// v 的类型 S 是不完整类型,程序是 ill-formed 。
// 若 S 的定义在当前翻译单元,则 S 是完整类型,则左值到右值转换是良构的。
/// >>> 3. 非类类型 T : 分类条件 >>>
const int x = 1;
int y = x; // 发生语境:用作初始化的源表达式时
// T = const int ,是非类类型。
// 纯右值的类型是 T 的 cv 未限定版本,即 int :const int x -> int t -> int y
// int y; y = x; // 发生语境:用作运算符的操作数时
// int y = +x; // 发生语境:用作运算符的操作数时
// 以上都会发生 cv T x -> T t -> T y 。
// 这里的 cv-qualifier 可以取 const, volatile, const volatile 。
/// >>> 4. 其他 T :分类条件 >>>
struct S { int n; };
const S s{1};
S t = s;
// T = const S ,是类类型,是“否则”分支,则纯右值的类型是 T 。
// const S s -> const S t -> const
// s 的 const 被保留,纯右值的类型是 const S
// 这里类类型有点特殊。
// 首先,类类型在移动/拷贝操作时先走重载决议,没机会经过走隐式转换,中间对象删除 cv 限定会干扰语义。
// 其次,对于运算符的操作数,可重载运算符依然是先走重载协议,而不可重载运算符比如成员访问运算符显然也必要保留 cv 限定。
当对表达式 E 应用左值到右值转换,且满足以下任一条件时:§7.3.1/2
- E 不是潜在求值的,或者
int x = 1; sizeof(x + 1); // + 是运算符,需要操作数是纯右值,对 x 应用左值到右值转换 // 但 x + 1 是 sizeof 的操作数,不是潜在求值的 // 所以不访问 x 的值,x 保持为左值 - E 的求值导致对 E 的潜在结果集合中某个成员的求值,并且该成员命名了一个未被 odr-used 的变量 x,
const int x = 5; int y = x; // 初始化 y 需要纯右值,对 x 应用左值到右值转换 // E = x,E 的潜在结果集合只包含 x // x 可在常量表达式中使用,不是 odr-used 。(参见本文第一章:odr-used) // 所以不访问 x 的对象,直接用值 5
被引用对象中包含的值不会被访问。
(左值到右值转换的)转换结果按以下规则确定:§7.3.1/3
- 如果 T 是 cv std::nullptr_t,结果是空指针常量。
// std::nullptr_t 是语言核心类型,也是标签类型。
// nullptr 是关键字,也是指针字面量,是纯右值。
std::nullptr_t n = nullptr;
std::nullptr_t m = n;
// n 是 std::nullptr_t 左值,m 的初始化对 n 应用左值到右值转换。
// T = std::nullptr_t,转换结果是空指针常量:std::nullptr_t n -> nullptr -> std::nullptr_t m .
- 否则,如果 T 有类类型,转换从泛左值拷贝初始化结果对象(默认构造)。
struct S { int n; };
S s{1};
S a = s; // 默认构造!!!
// s 是 S 左值,初始化 s 对 s 应用左值到右值转换。
// T = S 为类类型,转换从 s 拷贝初始化结果对象,结果对象是 a 。
// S s --(拷贝)--> S a 。
- 否则,如果泛左值所指对象包含无效指针值,行为由实现定义(UB,行为由编译器定义)。
int* p = new int(1);
delete p;
int* q = p;
// p 是 int* 左值
// 初始化 q 对 p 应用左值到右值转换
// T = int*
// p 所指对象已被释放,包含无效指针值
// 行为由实现定义
- 否则,读取泛左值所指对象,对象中包含的值是纯右值结果。
int x = 42;
int y = x;
// T = int ,读取 x 所指对象,对象中包含的值 42 是纯右值结果:泛左值 x -> 纯右值 “x 中的值 42” -> 泛左值 y 。
出于历史原因,此转换被称为“左值到右值”转换,尽管该名称并未准确反映 [basic.lval] 中描述的表达式分类。§7.3.1/4
- 更准确的名字应该是泛左值到纯右值的转换。
数组到指针转换
类型为“N 个 T 的数组”或“未知边界 T 的数组”的左值或右值可以转换为“指向 T 的指针”类型的纯右值(中间的无名表达式)。纯右值数组到纯右值指针时临时物化转换([conv.rval])被使用。结果是指向数组第一个元素的指针。§7.3.2/1
// 左值:元素类型为 T 的数组 -> 纯右值:指向 T 的指针
int arr[3] = {1, 2, 3};
int* p = arr;
// 数组 arr 是类型为 int[3] 的左值。
// 数组 arr 使用左值到右值转换得到 int* 类型的指针 t ,指向 arr[0] 。
// 指针 p 由 t 初始化。
// 泛左值 int[3] arr -> 纯右值 int* t -> 纯右值 int* p 。
// 纯右值值:元素类型为 T 的数组 -> 纯右值:指向 T 的指针
// 这是一个有趣到经典的例子。
using Int3 = int[3];
int* p = Int3{1, 2, 3};
// Int3{1,2,3} 是类型为 int[3] 的纯右值。
// 应用临时物化转换:创建 int[3] 类型的临时数组对象 t,用 {1,2,3} 初始化 t 。
// 临时物化转换同时产生将亡值 expr ,指代 t 。
// 此处 expr 再使用:”左值到右值转换,并用右值初始化左值“的步骤。
// 最终 p 被初始化,类型为 int* ,指向临时数组的第一个元素 t[0] 。
// 这相当于 `int *p = int[3]{1, 2, 3};`
// 注意:临时数组在完整表达式结束后销毁,此刻 p 成为悬垂指针。
// 这体现了隐式转换的理解对安全性的保证,不易察觉的语义都是攻击者可能使用的后门。
函数到指针转换
函数类型 T 的左值可以转换为“指向 T 的指针”类型的纯右值。结果是指向该函数的指针。此转换从不应用于非静态成员函数,因为无法获得引用非静态成员函数的左值。§7.3.3/1
void f();
void (*p)() = f;
// f 是函数类型左值
// 函数到指针转换:f 转换为 void(*)() 纯右值 t ,这里准寻左值到右值转换的规则。
// 纯右值 t 用于初始化 p ,类型是 void(*)() 。
注:非静态成员函数的左值无法获取,如上。
限定调整
限定转换
类型 T 的 cv 分解是一个 cv_i 和 P_i 的序列,使得 T 是“cv_0 P_0 cv_1 P_1 ⋯ cv_{n−1} P_{n−1} cv_n U”,其中 n ≥ 0,每个 cv_i 是一组 cv 限定符([basic.type.qualifier]),每个 P_i 是“指向”([dcl.ptr])、“指向类 C_i 的类型的成员的指针”([dcl.mptr])、“N_i 的数组”或“未知边界的数组”([dcl.array])。如果 P_i 指定数组,则元素类型上的 cv 限定符 cv_{i+1} 也被视为数组的 cv 限定符 cv_i 。§7.3.5/1
两个类型 T1 和 T2 是相似的:
- 如果它们的 cv 分解具有相同的 n,要么使得对应的 Pi 分量相同,
- 要么一个是“Ni 的数组”另一个是“未知边界的数组”,并且由 U 表示的类型相同。§7.3.5/2
两个类型 T1 和 T2 的 cv 组合类型是类似于 T1 的类型 T3 ,其 cv 分解满足:§7.3.5/3
- \(\forall i > 0. cv_{3i} = cv_{1i} \cup cv_{2i}\) .
- 如果
P_1i或P_2i是“未知边界的数组”,则P_3i是“未知边界的数组”;否则P_3i是P_1i; - 如果得到的
cv_3i不同于cv_1i或不同于cv_2i,或者得到的P_3i不同于P_1i或不同于P_2i,则给每个满足0 < k < i的cv_3k加上const。
其中cv_ji和P_ji是T_j的 cv 分解的组成部分。
以上,为标准复述的上下文。我们可以忠实地复述,当然也可以:
- cv 分解 is cv 分解,具体可以参考 https://www.cnblogs.com/03Goose/p/23103817 。
- cv 分解理论上可以很长,但一般的例子几乎都是简单的。
如果 T1 和 T2 的 cv 组合类型是 T2,则类型 T1 的纯右值可以转换为类型 T2。
- 除了上下文之外,这一句话是限定转换。
分割
下述内容我在实际场景中没有很清晰地提供过实现过,不负责作为其他博文的交叉引用。
先鸽……
或许等我写个小型编译器后再回来补?
函数指针转换
“指向 noexcept 函数的指针”类型的纯右值可以转换为“指向函数的指针”类型的纯右值。结果是指向该函数的指针。“指向 noexcept 函数类型的成员的指针”类型的纯右值可以转换为“指向函数类型的成员的指针”类型的纯右值。结果指定该成员函数。§7.3.13/1
提升
整形提升
除 bool、char16_t、char32_t、wchar_t 之外,整数转换等级小于 int 的整数类型纯右值,如果 int 能表示源类型的所有值,则转换为 int;否则转换为 unsigned int。§7.3.6/1
char16_t、char32_t、wchar_t 的纯右值可以转换为以下列表中第一个能表示其底层类型所有值的类型:int、unsigned int、long、unsigned long、long long、unsigned long long。§7.3.6/2
bool 类型的纯右值可以转换为 int 类型的纯右值,false 变为 0,true 变为 1。§7.3.6/3
底层类型未固定的无作用域枚举类型的纯右值,可以转换为以下列表中第一个能表示枚举所有值的类型:int、unsigned int、long、unsigned long、long long、unsigned long long。§7.3.6/4
底层类型固定的无作用域枚举类型的纯右值,可以转换为底层类型的纯右值。§7.3.6/5
浮点提升
float 类型的纯右值可以转换为 double 类型的纯右值。值不变。§7.3.7/1
转换
整形转换
整数类型的纯右值可以转换为另一种整数类型的纯右值。无作用域枚举类型的纯右值可以转换为整数类型的纯右值。§7.3.8/1
如果目标类型是无符号的,结果值是与源整数同余的最小无符号整数(模 2^n,其中 n 是用于表示无符号类型的位数)。如果目标类型是有符号的,如果值能在目标类型中表示,则值不变;否则,值由实现定义。§7.3.8/2
浮点转换
浮点类型的纯右值可以转换为另一种浮点类型的纯右值。如果源值能在目标类型中精确表示,转换结果就是该精确表示。如果源值位于两个相邻的目标值之间,转换结果是这两个值中任一个的实现定义选择。§7.3.9/1
整形-浮点转换
浮点类型的纯右值可以转换为整数类型的纯右值。转换截断,即丢弃小数部分。如果截断后的值不能在目标类型中表示,行为未定义。§7.3.10/1
指针转换
类型为“指向 cv T 的指针”(T 是对象类型)的纯右值,可以转换为“指向 cv void 的指针”类型的纯右值。指针值不变。§7.3.11/1
类型为“指向 cv D 的指针”(D 是完整类类型)的纯右值,可以转换为“指向 cv B 的指针”类型的纯右值,其中 B 是 D 的基类。如果 B 是 D 的不可访问或有歧义的基类,需要此转换的程序是 ill-formed 的。转换结果是指向派生类对象的基类子对象的指针。空指针值转换为目标类型的空指针值。§7.3.11/2
空指针常量是值为零的整数字面量或 std::nullptr_t 类型的纯右值。空指针常量可以转换为指针类型;结果是该类型的空指针值。§7.3.11/3
指针到成员转换
类型为“指向 B 的 cv T 类型成员的指针”(B 是类类型)的纯右值,可以转换为“指向 D 的 cv T 类型成员的指针”类型的纯右值,其中 D 是 B 的派生类。如果 B 是 D 的不可访问或有歧义的基类,需要此转换的程序是 ill-formed 的。转换结果引用与转换前相同的成员,但它引用 D 中的成员而非 B 中的成员。空成员指针值转换为目标类型的空成员指针值。§7.3.12/1
空成员指针常量是值为零的整数字面量或 std::nullptr_t 类型的纯右值。空成员指针常量可以转换为指针到成员类型;结果是该类型的空成员指针值。§7.3.12/2
布尔转换
算术类型、无作用域枚举类型、指针类型或指针到成员类型的纯右值可以转换为 bool 类型的纯右值。零值、空指针值或空成员指针值转换为 false;任何其他值转换为 true。对于直接初始化,std::nullptr_t 类型的纯右值可以转换为 bool 类型的纯右值;结果值为 false。§7.3.14/1
用户定义转换
上下文转换
隐式转换序列
注:在隐式转换序列中,我们将接触到那些在标准下,实际存在但又不可见的东西。
隐式转换序列是用于将函数调用中的实参转换为被调用函数相应形参类型的一系列转换。它受“通过单个表达式初始化对象或引用”的规则支配。§12.4.3.1/1
/// >>> 每个实参各自朝着对应形参的类型转换 >>>
void f(double d, const std::string& s);
f(1, "hi");
// 实参:声明符 `1` ,类型 `int` -> 形参:声明符 `d` ,类型 `double` 。
// 实参:声明符 `"hi"` ,类型 `char*` -> 形参:声明符 `s` ,类型 `const std::string&` 。
/// <<< 每个实参各自朝着对应形参的类型转换 <<<
/// >>> 单个表达式初始化的规则支配:按值形参 = 复制初始化 T param = arg; >>>
struct A { A(int); };
struct B { explicit B(int); };
void f(A);
void g(B);
f(1); // OK:A param = 1; 复制初始化,允许非 `explicit` 构造函数。
g(1); // 错误:B param = 1; 复制初始化,不允许 `explicit` 构造函数。
/// <<< 单个表达式初始化的规则支配:按值形参 = 复制初始化 T param = arg; <<<
隐式转换序列只关心实参的类型、cv 限定和值类别,以及这些属性如何被转换以匹配形参的相应属性。其他属性均被忽略。§12.4.3.1/2
/// >>> 隐式转换序列关心的属性:类型、cv 限定、值类别 >>>
// 1. 类型:int -> double
void f1(double);
f1(1); // 只看类型:int 能不能转成 double ?能。
// 2. cv 限定:const int 左值不能绑定 int&
void f2(int&);
void f3(const int&);
int x = 0;
const int cx = 0;
f2(x); // OK:int 左值 -> int& 。
f2(cx); // FAIL:const int 左值 -> int& 。
f3(x); // OK:int 左值 -> const int& 。
f3(cx); // OK:const int 左值 -> const int& 。
f3(1); // OK:int 右值 -> const int& 。
// 3. 值类别:右值可以绑定 int&&,左值不行。
void f4(int&&);
f4(1); // OK:int 右值 -> int&& 。
f4(x); // FAIL:int 左值 -> int&& 。
/// <<< 隐式转换序列关心的属性:类型、cv 限定、值类别 <<<
/// >>> 隐式转换序列忽略的其他属性:如可访问性 >>>
// 可访问性:私有构造函数
class A {
private:
A(int); // 私有构造函数
public:
A() = default;
};
void fa(A);
fa(1); // 隐式转换序列关心的:`int` 能不能变成 `A` ?能。
// 实际上:A(int) 不可访问,非良构。这不是隐式转换序列关心的范围。
/// <<< 隐式转换序列忽略的其他属性:如可访问性 <<<
一个良构的隐式转换序列是以下形式之一:标准转换序列、用户定义转换序列、或省略号转换序列。§12.4.3.1/3
// 1. 标准转换序列:语言内置的转换
void f(double);
f(1); // int -> double
void g(const int*);
int* p = nullptr;
g(p); // int* -> const int*
struct Base {};
struct Derived : Base {};
void h(Base);
Derived d;
h(d); // Derived -> Base
// 2. 用户定义转换序列:涉及类的构造函数或转换函数
// 2.1 非 `explicit` 说明的单参数构造函数。
struct A { A(int); };
void fa(A);
fa(1); // int -> A,通过 A(int)
// 2.2 非 `explicit` 说明的类型转换运算符。
struct B { operator int(); };
void fb(int);
B b;
fb(b); // B -> int,通过 operator int()
// 3. 省略号转换序列:传给 ...
void fc(...);
fc(1); // 1 -> ...
标准转换序列

表 16 总结了 [conv] 中定义的转换,并将其划分为四个互不相交的类别:左值转换、限定调整、提升、转换。§112.4.3.1.1/1
如 [conv] 所述,一个标准转换序列要么是单独的身份转换(即没有转换),要么由来自其他四个类别的一到三个转换组成。如果序列中有两个或更多转换,转换按规范顺序应用:左值转换、提升或转换、限定调整。§112.4.3.1.1/2
表 16 中的每个转换还关联了一个等级(精确匹配、提升或转换)。这些等级用于对标准转换序列进行排序。转换序列的等级通过考虑序列中每个转换的等级以及任何引用绑定的等级来确定。如果其中任何一个具有转换等级,则序列具有转换等级;否则,如果其中任何一个具有提升等级,则序列具有提升等级;否则,序列具有精确匹配等级。§112.4.3.1.1/3
左值转换
用户定义转换序列
用户定义转换序列由一个初始标准转换序列、一个用户定义转换和一个第二标准转换序列组成。如果用户定义转换由构造函数指定,则初始标准转换序列将源类型转换为该构造函数参数所要求的类型。如果用户定义转换由转换函数指定,则初始标准转换序列将源类型转换为该转换函数的隐式对象参数。§112.4.3.1.2/1
这些规则阻止在重载解析期间应用超过一个用户定义转换,从而避免无限递归。§112.4.3.1/4
第二标准转换序列将用户定义转换的结果转换为序列的目标类型。由于隐式转换序列是一种初始化,当为用户定义转换序列选择最佳用户定义转换时,适用于用户定义转换初始化的特殊规则。§112.4.3.1.2/2
将类类型的表达式转换为同一类类型被赋予精确匹配等级,将类类型的表达式转换为该类型的基类被赋予转换等级,尽管在这些情况下会调用构造函数。§112.4.3.1.2/4
省略号转换序列
当函数调用中的实参与被调用函数的省略号参数规格匹配时,发生省略号转换序列。§112.4.3.1.3/1
排序
When comparing the basic forms of implicit conversion sequences (as defined in [over.best.ics])
a standard conversion sequence is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and
a user-defined conversion sequence is a better conversion sequence than an ellipsis conversion sequence. §112.4.3.2/2
Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if
S1 is a proper subsequence of S2 (comparing the conversion sequences in the canonical form defined by [over.ics.scs], excluding any Lvalue Transformation; the identity conversion sequence is considered to be a subsequence of any non-identity conversion sequence) or, if not that,
the rank of S1 is better than the rank of S2, or S1 and S2 have the same rank and are distinguishable by the rules in the paragraph below, or, if not that,
S1 and S2 are reference bindings and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference. §112.4.3.2/3
浙公网安备 33010602011771号