# 类型转换重载
这一节我们要讨论的是如何重载 类型转换运算符 。不过在此之前,我们首先总结一下类型转换的相关知识。
# 类型转换回顾
C++ 作为弱类型语言,其类型转换发生得非常频繁。其中,类型转换主要分为两类:
- 显式类型转换:我在程序中明面上提出要做转换,通过使用类型转换运算符形成表达式;
- 隐式类型转换:编译器发现某个类型不适合这个语境,从而自动发生的类型转换。
显式类型转换很好理解,就是我手动地写下 形如
类型
(
表达式
)
或
(
类型
)
表达式
这样的表达式,来让
表达式
做转换到
类型
这个类型。想必我也不用再次强调了,类型转换并不更改表达式的类型,而是运算出一个新的临时结果。
而隐式类型转换发生的场合则比较多。我们已经知道的,在以下时机会发生从
A
类型到
B
类型的转换:
-
用
A
类型初始化B
类型的变量。这其中又包括:-
声明并定义
B
类型变量,其中初始化值是A
类型的; -
函数形参期望是
B
类型的,而实参是A
类型的; -
函数返回值类型期望是
B
,而return
语句中的表达式是A
类型的;
-
声明并定义
-
某个运算符期待
B
类型操作数,但实际给出的操作数却是A
类型的; -
在
if
while
和for
的条件表达式中,可以将A
类型转换到bool
。
# 从类类型转换到别的类型
假设我们要构造从
A
到
B
类型的类型转换,而
A
是类(也就是我们可以“控制”的),那么就可以定义这样一个运算符重载:
class A {
public:
operator B() {
// 语句 ...
return /* B 类型的值 */;
这里出现的语法就是重载类型转换运算符的语法了:
operator 类型名 () 函数体
这里,
operator
类型名
的写法和之前的什么
operator+=
啊
operator[]
啊是很像的;而且它不需要参数——因为类型转换是一元运算符,不再需要额外的右操作数了。但不一样的地方是它
没有返回值类型
。这是因为,转换到
B
类型的运算符重载必然返回
B
类型,所以不用再次强调。
我们还是以
String
类举例。这一次,我们定义从
String
到
bool
的转换:如果
String
为空字符串
""
则转换到
false
;否则,转换到
true
。也就是说效果类似:
#include <iostream>
int main() {
String a;
// [...]
if (a) { // 这里调用从 String 到 bool 的隐式转换
std::cout << "String a is not empty!" << std::endl;
} else {
std::cout << "String a is empty. Aborted." << std::endl;
// 当然你也可以显式转换
bool isEmpty;
isEmpty = !bool(a);
那么实现过程只要套用上面的语法就可以了:
class String {
public:
// [...]
operator bool() {
if (len == 0) return false;
else return true;
# 从别的类型转换到类类型
接下来我们尝试反过来:如果转换到的类型是类类型,但转换前的类型无法控制(比如是一个
int
之类的),那么该怎么做呢?
如果用
String
类举例的话,我想定义从一个 C 风格字符串到
String
的转换。C 风格字符串是
const char[N]
类型的,而
N
是未知的,所以为了简便起见就用
const char*
类型好了。那么我们想要做的效果就是:
#include <iostream>
// 截止目前的 String 类定义:https://paste.ubuntu.com/p/d4HYm4cZ4h/
int main() {
char a[]{"Hello"};
String b;
b = a; // 赋值运算符右侧期待 String,但传入 const char* 发生转换
std::cout << b.str << std::endl; // 应输出 "Hello"
诶诶诶,你会神奇的发现:我们什么都没做怎么就编译通过了?而且运行的结果还是正确的!
这是因为,我们定义了这个东西:
class String {
public:
String(const char* initVal);
这是参数列表只有一个
const char*
的构造函数。而这个构造函数实际上同样定义了从
const char*
到
String
的转换;而转换的过程恰恰就是调用构造函数的过程。所以说,有了这个构造函数我们可以做隐式类型转换:
int main() {
String a;
a = "abc"; // 从 const char* 隐式转换到 String,然后调用赋值运算符重载
也可以做显式类型转换:
int main() {
String a;
a = String("abc"); // String("abc") 是类型转换表达式
而且神奇地,这种类型转换表达式的写法和构造函数的定义神似。也有人管这种类型转换表达式叫做“构造临时量”。类似地,我们也可以将“零个值”转换为
String
(通过调用默认构造函数)或者将多于一个的值转换为
String
:
int main() {
String a("Hello");
a = String(); // 现在 a 是 ""
a = String(5, '@'); // 现在 a 是 "@@@@@"
目前而言,我们学过的所有构造函数都可以用于隐式或显式的类型转换。可以用于隐式类型转换的构造函数又被称为
转换构造函数
(即非
explicit
说明的构造函数,见下文)。
类似地,也有构造聚合初始化临时值的列表风格转型运算符
类型 { 值列表 }
。它会用值列表
的值来大括号(聚合)初始化类型
变量,将初始化得到的对象作为表达式的结果。
#
explicit
关键字
最后我们引入
explicit
关键字。我们注意到隐式转换发生的过于频繁(而且可以有很多意想不到的隐式转换)。比如刚刚只定义了从
const char*
到
String
的转换,而刚刚
a = "abc"
这个表达式其实是先从
const char[N]
转换到
const char*
,然后再转换到
String
的。这表明,在 C++ 里面,同一个地方可以允许非常多次的转换。
另一方面,我们又定义了从
String
到
bool
的转换。但
bool
能转换到
int
,这就导致:
int main() {
String a("42");
int val{a}; // 猜猜 val 是多少?
显然
val
不能是
42
。
val
的值是从
bool
类型转换过来的,而
String
值
"42"
转换到
bool
是
true
,
true
转换到
int
是
1
,所以答案是
1
。这个神奇的过程在某种程度上会加大编程者的心智负担,所以我们并不希望如此自由的转换。
于是
explicit
关键字就呼之欲出了。它表明:这个转换
只能用于显式转换
,不能用于隐式转换。
struct B {};
struct C {};
struct A {
A() { }
explicit A(const C&) { } // 只允许显式的从 C 到 A 的转换
explicit operator B() { return B(); } // 只允许显式的从 A 到 B 的转换
int main() {
A a; B b; C c;
// a = c; // 不许隐式转换
a = A(c); // 但可以显式转换
// b = a; // 不许隐式转换
b = B(a); // 但可以显式转换
对于我们而言,我们不想要隐式的从
String
到
bool
的转换,所以把它标记为
explicit
的。但这是不是就引发了这样的问题:我们没有办法直接在
if
里面用
String
了?
int main() {
String a;