浅析 explicit 关键字
前言
C++ 提供了多种方式来实现类型转换和构造对象,然而,有时候这些方式会导致一些意想不到的结果,比如隐式转换
和复制初始化
。为了避免这些潜在的问题,C++ 引入了 explicit 关键字,它可以限制一些不必要或不安全的转换和初始化。在本文中,我们将介绍 explicit 关键字的作用和用法,以及它如何提高代码的可读性和安全性。
案例剖析
为了说明 explicit 关键字的作用和用法,我们首先定义两个结构体 A 和 B,它们都有一个接受 int 参数的构造函数和一个返回 bool 值的转换函数,但是 B 的构造函数和转换函数都被 explicit 修饰了,而 A 的没有。
struct A
{
A(int) {}
operator bool() const { return true; }
};
struct B
{
explicit B(int) {}
explicit operator bool() const { return true; }
};
接下来,我们定义两个函数 doA 和 doB,它们分别接受 A 和 B 类型的参数。
void doA(A a) {}
void doB(B b) {}
然后,我们在 main 函数中创建一些 A 和 B 类型的对象,并尝试用不同的方式进行初始化和转换。
int main() {
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化
A a3{1}; // OK:直接列表初始化
A a4 = {1}; // OK:复制列表初始化
A a5 = (A)1; // OK:允许 static_cast 的显式转换
doA(1); // OK:允许从 int 到 A 的隐式转换
if (a1)
; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化
B b1(1); // OK:直接初始化
// B b2 = 1; // 错误:被 explicit
// 修饰构造函数的对象不可以复制初始化
B b3{1}; // OK:直接列表初始化
// B b4 = { 1 }; // 错误:被 explicit
// 修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK:允许 static_cast 的显式转换
// doB(1); // 错误:被 explicit
// 修饰构造函数的对象不可以从 int 到 B 的隐式转换
if (b1)
; // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool
// 的按语境转换
bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B
// 到 bool 的按语境转换
// bool b7 = b1; // 错误:被 explicit 修饰转换函数
// B::operator bool() 的对象不可以隐式转换
bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化
return 0;
}
从上面的代码中,我们可以看出以下几点:
- explicit 修饰的构造函数只能用于
直接初始化
和直接列表初始化
,不能用于复制初始化和复制列表初始化。这样可以避免一些不必要的类型转换,比如将一个 int 值赋给一个 B 类型的变量,或者将一个花括号列表赋给一个 B 类型的变量。- explicit 修饰的转换函数只能用于
显式转换和按语境转换
,不能用于隐式转换。这样可以避免一些不安全的类型转换,比如将一个 B 类型的对象赋给一个 bool 类型的变量,或者将一个 B 类型的对象作为条件表达式。按语境转换是指在某些特定的语境中,编译器会自动调用 explicit 修饰的转换函数,比如 if 语句,while 语句,for 语句,switch 语句,逻辑运算符,条件运算符,static_cast,和 bool 构造函数。- explicit 关键字可以提高代码的清晰度和一致性,避免一些潜在的错误和歧义。
补充案例
上面案例中A、B构造函数都属于单参数类型,下面印证仅有多参数构造函数的类是否存在同样的结论。此处以“分数”为例构建结构体案例:
例如,我们可以定义一个表示分数的结构体 Fraction,它有一个接受两个 int 参数的构造函数,表示分子和分母,以及一个返回 double 值的转换函数,表示分数的小数值。
我们可以用 explicit 关键字来修饰这两个函数,以防止一些不合理或不明确的转换和初始化。
struct Fraction
{
int num; // 分子
int den; // 分母
// explicit 修饰的构造函数,防止从 int 或 int,int 到 Fraction 的隐式转换和复制初始化
explicit Fraction(int n, int d = 1) : num(n), den(d) {}
// explicit 修饰的转换函数,防止从 Fraction 到 double 的隐式转换
explicit operator double() const { return static_cast<double>(num) / den; }
};
接着创建一些 Fraction 类型的对象,并尝试用不同的方式进行初始化和转换。
Fraction f1(1, 2); // OK:直接初始化
// Fraction f2 = 1; // 错误:被 explicit 修饰的构造函数不可以复制初始化
// Fraction f3 = 1, 2; // 错误:被 explicit 修饰的构造函数不可以复制初始化
Fraction f4{ 1, 2 }; // OK:直接列表初始化
// Fraction f5 = { 1, 2 }; // 错误:被 explicit 修饰的构造函数不可以复制列表初始化
Fraction f6 = (Fraction)1; // OK:允许 static_cast 的显式转换
Fraction f7 = static_cast<Fraction>(1); // OK:static_cast 进行直接初始化
// double d1 = f1; // 错误:被 explicit 修饰的转换函数不可以隐式转换
double d2(f1); // OK:被 explicit 修饰的转换函数可以按语境转换
double d3 = static_cast<double>(f1); // OK:static_cast 进行直接初始化
我们看到上面 f6、f7 对象通过类型强转生成 Fraction 对象这个过程属于显式转换
,所以经过强转生成的临时对象可以成功执行拷贝构造和赋值运算函数将值传给 f6、f7 对象。
- explicit 修饰的构造函数可以防止从 int 或 int,int 到 Fraction 的隐式转换和复制初始化。例如,如果我们想要创建一个表示 1/2 的分数,我们应该使用 Fraction f(1, 2) 或 Fraction f{1, 2},而不是 Fraction f = 1 或 Fraction f = {1, 2},因为后者会创建一个表示 1/1 或 2/1 的分数,这显然不是我们的目的。
- explicit 修饰的转换函数可以防止从 Fraction 到 double 的隐式转换。例如,如果我们想要计算一个分数的倒数,我们应该使用
static_cast<double>(f)
来显式地将分数转换为 double 类型,然后再进行除法运算,而不是直接使用 1 / f,因为后者会先将 1 隐式地转换为 Fraction 类型,然后再调用 Fraction 类的重载的除法运算符,这可能会得到一个错误的结果。
总结
- explicit 修饰
构造函数
时,可以防止隐式转换和复制初始化- explicit 修饰
转换函数
时,可以防止隐式转换,但按语境转换除外
在本文中介绍了 explicit 关键字的作用和用法,以及它如何提高代码的可读性和安全性。我们了解了 explicit 关键字可以用于修饰构造函数和转换函数,以阻止隐式转换和复制初始化,也了解了 explicit 修饰的构造函数和转换函数的使用场景和限制。我们认识到 explicit 关键字可以帮助我们避免一些不必要或不安全的类型转换,提高代码的清晰度和一致性。希望本文对你有所帮助,如果你有任何问题或建议,欢迎在评论区留言。谢谢!