类型转换重载

这一节我们要讨论的是如何重载类型转换运算符。不过在此之前,我们首先总结一下类型转换的相关知识。

类型转换回顾

C++ 作为弱类型语言,其类型转换发生得非常频繁。其中,类型转换主要分为两类:

  • 显式类型转换:我在程序中明面上提出要做转换,通过使用类型转换运算符形成表达式;
  • 隐式类型转换:编译器发现某个类型不适合这个语境,从而自动发生的类型转换。

显式类型转换很好理解,就是我手动地写下 形如 类型(表达式)(类型)表达式 这样的表达式,来让 表达式 做转换到 类型 这个类型。想必我也不用再次强调了,类型转换并不更改表达式的类型,而是运算出一个新的临时结果。

而隐式类型转换发生的场合则比较多。我们已经知道的,在以下时机会发生从 A 类型到 B 类型的转换:

  • A 类型初始化 B 类型的变量。这其中又包括:
    • 声明并定义 B 类型变量,其中初始化值是 A 类型的;
    • 函数形参期望是 B 类型的,而实参是 A 类型的;
    • 函数返回值类型期望是 B,而 return 语句中的表达式是 A 类型的;
  • 某个运算符期待 B 类型操作数,但实际给出的操作数却是 A 类型的;
  • if whilefor 的条件表达式中,可以将 A 类型转换到 bool

从类类型转换到别的类型

假设我们要构造从 AB 类型的类型转换,而 A 是类(也就是我们可以“控制”的),那么就可以定义这样一个运算符重载:

class A {
public:
    operator B() {
        // 语句 ...
        return /* B 类型的值 */;
    }
};

这里出现的语法就是重载类型转换运算符的语法了:

operator 类型名 () 函数体

这里,operator 类型名 的写法和之前的什么 operator+=operator[] 啊是很像的;而且它不需要参数——因为类型转换是一元运算符,不再需要额外的右操作数了。但不一样的地方是它没有返回值类型。这是因为,转换到 B 类型的运算符重载必然返回 B 类型,所以不用再次强调。

我们还是以 String 类举例。这一次,我们定义从 Stringbool 的转换:如果 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++ 里面,同一个地方可以允许非常多次的转换。

另一方面,我们又定义了从 Stringbool 的转换。但 bool 能转换到 int,这就导致:

int main() {
    String a("42");
    int val{a}; // 猜猜 val 是多少?
}

显然 val 不能是 42val 的值是从 bool 类型转换过来的,而 String"42" 转换到 booltruetrue 转换到 int1,所以答案是 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); // 但可以显式转换
}

对于我们而言,我们不想要隐式的从 Stringbool 的转换,所以把它标记为 explicit 的。但这是不是就引发了这样的问题:我们没有办法直接在 if 里面用 String 了?

int main() {
    String a;
    if (a) { } // a 不能隐式转换到 bool 了?
}

实际上这个问题并不存在。因为 C++ 提供了一个例外判断,如果在形如:

  • if for while 条件;
  • && || ! 操作数,?: 第一操作数;

等这样的条件下,则也考虑显式类型转换。这种例外判断称为“按语境转换到 bool”。

最近更新:
代码未运行