文章

关键字 Explicit

关键字 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

本文由作者按照 CC BY 4.0 进行授权