类型转换重载
这一节我们要讨论的是如何重载类型转换运算符。不过在此之前,我们首先总结一下类型转换的相关知识。
类型转换回顾
C++ 作为弱类型语言,其类型转换发生得非常频繁。其中,类型转换主要分为两类:
- 显式类型转换:我在程序中明面上提出要做转换,通过使用类型转换运算符形成表达式;
- 隐式类型转换:编译器发现某个类型不适合这个语境,从而自动发生的类型转换。
显式类型转换很好理解,就是我手动地写下 形如 类型(表达式)
或 (类型)表达式
这样的表达式,来让 表达式
做转换到 类型
这个类型。想必我也不用再次强调了,类型转换并不更改表达式的类型,而是运算出一个新的临时结果。
而隐式类型转换发生的场合则比较多。我们已经知道的,在以下时机会发生从 A
类型到 B
类型的转换:
- 用
A
类型初始化B
类型的变量。这其中又包括:- 声明并定义
B
类型变量,其中初始化值是A
类型的; - 函数形参期望是
B
类型的,而实参是A
类型的; - 函数返回值类型期望是
B
,而return
语句中的表达式是A
类型的;
- 声明并定义
- 某个运算符期待
B
类型操作数,但实际给出的操作数却是A
类型的; - 在
if
while
和for
的条件表达式中,可以将A
类型转换到bool
。
从类类型转换到别的类型
假设我们要构造从 A
到 B
类型的类型转换,而 A
是类(也就是我们可以“控制”的),那么就可以定义这样一个运算符重载:
这里出现的语法就是重载类型转换运算符的语法了:
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"
}
诶诶诶,你会神奇的发现:我们什么都没做怎么就编译通过了?而且运行的结果还是正确的!
这是因为,我们定义了这个东西:
这是参数列表只有一个 const char*
的构造函数。而这个构造函数实际上同样定义了从 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
,这就导致:
显然 val
不能是 42
。val
的值是从 bool
类型转换过来的,而 String
值 "42"
转换到 bool
是 true
,true
转换到 int
是 1
,所以答案是 1
。这个神奇的过程在某种程度上会加大编程者的心智负担,所以我们并不希望如此自由的转换。
于是 explicit
关键字就呼之欲出了。它表明:这个转换只能用于显式转换,不能用于隐式转换。
对于我们而言,我们不想要隐式的从 String
到 bool
的转换,所以把它标记为 explicit
的。但这是不是就引发了这样的问题:我们没有办法直接在 if
里面用 String
了?
实际上这个问题并不存在。因为 C++ 提供了一个例外判断,如果在形如:
if
for
while
条件;&&
||
!
操作数,?:
第一操作数;
等这样的条件下,则也考虑显式类型转换。这种例外判断称为“按语境转换到 bool
”。