关键字 Explicit
[TOC]
C++
explicit:从隐式转换到实际使用
explicit 最常见的用途,是修饰构造函数,阻止编译器通过这个构造函数进行隐式转换。
先记住一句话:你仍然可以主动调用构造函数,但编译器不能在需要类型转换时自动替你调用它。
1. 什么是隐式转换
隐式转换,就是你没有明确写出转换操作,编译器自动把一种类型转换成另一种类型。
1
2
int a = 10;
double b = a; // int 自动转换成 double,b 的值为 10.0
如果明确写出转换操作,就叫显式转换:
1
double b = static_cast<double>(a);
这里两种写法结果相同,区别在于是否明确写出了类型转换。
隐式转换也可能丢失数据:
1
2
double price = 12.8;
int n = price; // n 为 12,小数部分被丢弃
注意:explicit 不是禁止所有类型转换的总开关。它约束的是被它修饰的构造函数或转换函数。
2. 构造函数为什么与类型转换有关
看一个可以用整数创建的类:
1
2
3
4
5
6
class Person {
public:
int age;
Person(int n) : age(n) {}
};
其中 : age(n) 是成员初始化列表,表示使用参数 n 初始化成员 age。
正常情况下,我们可以明确调用构造函数:
1
2
Person p1(18);
Person p2{18};
但没有 explicit 时,下面这种写法也可以:
1
Person p3 = 18;
左边需要一个 Person,右边却是 int。编译器发现 Person 有接收 int 的构造函数,于是允许通过它完成从 int 到 Person 的转换。
这里虽然有等号,但 p3 是正在创建的新对象,所以这是初始化,不是在给一个已有对象调用赋值运算符。也不要理解成一定会先创建临时对象,再发生一次拷贝。
函数传参同样可能触发转换:
1
2
3
4
5
void greet(Person p) {
// 使用 p
}
greet(18); // 自动把 18 转换成 Person
3. 加上 explicit 会发生什么
1
2
3
4
5
6
class Person {
public:
int age;
explicit Person(int n) : age(n) {}
};
明确构造仍然可以:
1
2
3
Person p1(18); // 可以:直接初始化
Person p2{18}; // 可以:直接列表初始化
Person p3 = Person(18); // 可以:右边已经明确构造了 Person
下面这些写法则不允许:
1
2
Person p4 = 18; // 报错:不允许通过该构造函数隐式转换
Person p5 = {18}; // 报错:这种初始化不能使用 explicit 构造函数
函数传参也要写清楚:
1
2
3
4
void greet(Person p) {}
greet(18); // 报错
greet(Person{18}); // 可以:明确构造后再传参
explicit 不会改变构造函数内部的逻辑,也不会让它少分配内存或提高运行速度。它主要在编译阶段限制哪些写法被允许。
4. 一般在什么情况下使用
4.1 参数表示大小、容量或数量
1
2
3
4
5
6
7
8
class Buffer {
public:
explicit Buffer(int size) {
// 根据 size 创建缓冲区
}
};
void send(const Buffer& buffer) {}
如果不加 explicit,send(1024) 也能编译:编译器自动创建一个 Buffer,再传入函数。但只看调用,很难知道 1024 表示缓冲区大小。
加上 explicit 后:
1
2
send(1024); // 报错
send(Buffer{1024}); // 可以,含义更清楚
4.2 封装用户 ID 等业务类型
1
2
3
4
5
6
7
8
9
class UserId {
public:
explicit UserId(int value) : value_(value) {}
private:
int value_;
};
void deleteUser(UserId id) {}
调用时要求明确说明这是用户 ID:
1
2
deleteUser(100); // 报错
deleteUser(UserId{100}); // 可以
这样可以避免普通整数被直接当成 UserId 传入。它不负责检查 100 是否为合法用户 ID,业务校验仍需要另外实现。
4.3 构造函数虽然有多个参数,但可以只传一个
1
2
3
4
5
6
class Buffer {
public:
explicit Buffer(int size, char fill = '\0') {
// 创建缓冲区
}
};
第二个参数有默认值,因此仍然可以写 Buffer b(1024)。这类构造函数也要考虑是否需要 explicit。
所以判断时不要只数参数总数,要看它能否用一个实参调用。
5. 另一个常见用途:explicit operator bool
有些对象需要支持判断“是否有效”,例如连接对象、文件句柄、智能指针。
1
2
3
4
5
6
7
8
9
class Connection {
public:
explicit operator bool() const {
return connected_;
}
private:
bool connected_ = true;
};
operator bool() 是类型转换函数,表示如何把 Connection 转换成 bool。它没有单独写返回类型,因为目标类型已经写在 operator 后面。
使用效果:
1
2
3
4
5
6
7
8
9
Connection conn;
if (conn) { // 可以:在条件中判断连接是否有效
// ...
}
bool a = conn; // 报错:不允许这种隐式转换
bool b = static_cast<bool>(conn); // 可以:明确转换
bool c(conn); // 可以:直接初始化
为什么 if 可以?因为 C++ 对条件中的 bool 转换有专门规则,允许使用 explicit operator bool。类似的场景还包括 while (conn)、!conn 等。
它也能防止对象通过 bool 进一步参与不合适的整数运算:
1
int n = conn; // 报错
这项功能从 C++11 开始支持。标准库智能指针就提供了 explicit operator bool,因此可以写 if (ptr) 判断是否非空。
6. 是不是所有构造函数都应该加 explicit
不需要机械地全部添加。关键看你是否希望提供自动转换这个功能。
例如,std::string 允许从字符串字面量构造:
1
2
3
4
5
6
#include <string>
void printName(const std::string& name) {}
std::string name = "张三";
printName("张三");
这里自动创建字符串对象很自然,也使接口更方便。
相反,缓冲区大小、用户 ID 等场景,自动转换容易掩盖调用含义,通常适合加 explicit。
一个实用习惯是:对于能够用一个实参调用的构造函数,优先考虑 explicit;只有明确希望允许隐式转换时,才省略它。
补充:explicit 并不限于单参数构造函数。C++11 起,它也可以修饰多参数构造函数,影响 T x = {a, b} 这样的拷贝列表初始化。初学时先掌握前面最常见的用法即可。
7. 常见误区与速查
| 容易误解的说法 | 准确理解 |
|---|---|
| explicit 禁止调用构造函数 | 它允许明确构造,限制隐式转换等初始化方式 |
| explicit 会提高运行效率 | 它主要限制编译时的合法写法 |
| 有等号就是赋值 | Person p = 18 是新对象的初始化 |
| 用花括号就一定能调用 explicit 构造函数 | Person p{18} 可以,Person p = {18} 不可以 |
| explicit 能确保用户 ID 合法 | 它只限制类型转换,合法性还需要校验 |
| explicit 只能修饰构造函数 | 也能修饰 operator bool 等转换函数 |
假设存在 explicit Person(int),常见写法的结果如下:
| 写法 | 能否编译 | 原因 |
|---|---|---|
Person p(18); |
可以 | 直接初始化 |
Person p{18}; |
可以 | 直接列表初始化 |
Person p = 18; |
不可以 | 不允许通过该构造函数隐式转换 |
Person p = {18}; |
不可以 | 拷贝列表初始化不能使用 explicit 构造函数 |
greet(18); |
不可以 | 实参不能隐式转换成 Person |
greet(Person{18}); |
可以 | 明确构造 Person 后再传入 |
8. 面试时怎么回答
explicit 主要用于修饰构造函数和类型转换函数,限制隐式转换。最常见的是给能够用一个参数调用的构造函数加 explicit,防止普通值被自动转换成类对象,例如禁止 Person p = 18,但仍然允许 Person p(18) 和 Person p{18}。另一个常见用途是 explicit operator bool,让对象可以在 if 中判断是否有效,同时限制普通的隐式转换。
记忆时抓住两个例子就行:构造函数里的 explicit 防止“数字自动变对象”;explicit operator bool 支持“对象判断有效性”,同时限制其他不必要的转换。
参考12
-
来源:ChatGPT ↩︎