文章首发
【重学 C++】06 | C++ 该不该应用 explicit
引言
大家好,我是只讲技术干货的会玩 code,明天是【重学 C ++】的第六讲,在 C++ 中,explicit
关键字作用于类的构造函数或类型转换操作符,以禁止隐式类型转换。明天,咱们来聊聊到底该不该应用explicit
。
explicit 的作用
在 C ++ 中,默认容许隐式转换,隐式类型转换是指在表达式中主动进行的类型转换,无需显式地指定转换操作。
struct Im {Im();
Im(int);
};
void read_im(const Im&);
int main(int argc, char const *argv[])
{
Im i1;
Im i2 = Im();
Im i3 = Im(1);
Im i4 = {};
Im i5 = 1;
Im i6 = {1};
read_im({});
read_im(1);
read_im({1});
}
下面的 i4
、i5
、i6
以及前面的 read_im
的调用都是隐式转换,以 i5
为例,可能将整数 1
转换成Im(1)
。
应用 explicit
关键字润饰类的构造函数,禁止隐式类型转换后,在进行类型转换时必须显式地指定转换操作。
struct Ex {explicit Ex();
explicit Ex(int);
};
void read_ex(const Ex&);
int main(int argc, char const *argv[]) {
Ex e1;
Ex e2 = Ex();
Ex e3 = Ex(1);
Ex e4 = 1; // error
read_ex(Ex());
read_ex(Ex(1));
read_ex(1); // error
}
隐式转换问题
隐式转换尽管看起来比拟便当,但升高了代码的可读性。并且,在一些状况下,这种转换会导致意外的后果,造成代码谬误。
精度失落
当将一个高精度的数据类型转换为低精度的类型时,可能会导致数据精度的失落,还是以下面 Im
数据结构为例。
struct Im {Im();
Im(int);
};
// 将浮点数 1.6 赋值给了 i, 失落了小数点后的精度
Im i = 1.6;
调用指标函数凌乱
假如我的项目中有这样一段代码
class Book {
std::string title_;
std::string author_;
public:
Book(std::string t, std::string a) :
title_(t), author_(a) {};};
void add_to_library(const Book&) {std::cout << "call exactly fn" << std::endl;}
template<class T = std::string>
void add_to_library(std::pair<bool, const T> param) {std::cout << "call template fn" << std::endl;}
int main(int argc, char const *argv[]) {add_to_library({"title", "author"});
}
代码输入:
call exactly fn
因为 Book
容许隐式转换,{"title", "author"}
被转换成了 Book("title", "author")
, 所以,最终会匹配到void add_to_library(const Book&)
, 目前看所有都很完满,但前面迭代后发现,Book
还应该有个 pages_
页数的成员变量。变更后的 Book
类定义如下:
class Book {
std::string title_;
std::string author_;
int pages_;
public:
Book(std::string t, std::string a, int p) :
title_(t), author_(a), pages_(p) {}};
改完 Book
的定义后,间接编译代码,发现是能够编译通过的,但再看下代码输入:
call template fn
因为 Book
减少了 pages_
成员变量,{"title", "author"}
无奈隐式转换成 Book
对象,所以,会持续匹配到模板函数 void add_to_library(std::pair<bool, const T> param)
。这种谬误比拟费解,在编译过程中也不会有任何warning
提醒。
对象被谬误回收
经典例子就是智能指针了,咱们在《03 |手撸 C ++ 智能指针实战教程》一节中也提到过,上面咱们再来回顾一下。
template <typename T>
class smart_ptr {
public:
// explicit smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
~smart_ptr() {delete ptr_;}
T& operator*() const { return *ptr_;}
T* operator->() const { return ptr_;}
private:
T* ptr_;
}
void foo(smart_ptr<int> int_ptr) {// ...}
int main() {int* raw_ptr = new int(42);
// 隐式转换为 smart_ptr<int>
foo(raw_ptr);
// error: raw_ptr 曾经被回收了
std::cout << *raw_ptr << std::endl;
// ...
}
假如咱们没有为 smart_ptr
构造函数加上 explicit
,原生指针raw_ptr
在传给 foo
函数后,会被隐形转换为 smart_ptr<int>
,foo
函数调用完结后,析构入参的 smart_ptr<int>
时会把 raw_ptr
给回收掉了,所以后续对 raw_ptr
的调用都会失败。
operator bool
谬误转换
C++ 中,有种 operator TypeName()
的语法,用来将对象转换为指定的 TypeName 类型。
class Foo {
public:
operator bool() const {return true;}
operator int() const {return 1;}
};
int main(int argc, char const *argv[]) {
Foo foo;
// ok
bool a = foo;
// ok
int b = foo;
}
这种类型转换个别没什么意义,反而会减少代码可读性。而且,有些时候可能还会呈现一些不容易发现的谬误。
Foo foo1;
Foo foo2;
if (foo1 = foo2) {std::cout << "foo1 equal foo2" << std::endl;}
这段代码,咱们本意是想要判断 foo1
与 foo2
是否相等,但少写了一个 =
, 因为 Foo
能隐式转换成 bool
类型,所以表达式 foo1 = foo2
的后果永远是 true
。
所以个别不倡议应用 operator Typename()
。如果的确有须要,应用前先思考是否能够加上explicit
禁止隐式转换,尤其是operator bool()
,C++ 为布尔转换留了 ” 后门 ”。
class ExFoo {
public:
explicit operator bool() const {return true;}
};
int main(int argc, char const *argv[]) {
ExFoo foo1;
// ok
if (foo1) {std::cout << "..." << std::endl;}
// error
bool a = foo1;
}
即便应用 explicit
,还是能够应用foo1 ? xxx : yyy
这种不便的三元运算符。同时禁止了bool a = foo1
这种无意义并且有隐患的类型转换。
所以,大部分状况下,咱们都举荐应用 explicit
禁止默认的隐式转换,能够使代码更加强壮,升高潜在的谬误和意外行为的危险。
当然,有几种非凡的状况,容许隐式转换是比拟适合的。
隐式转换正当应用场景
拷贝构造函数和挪动构造函数
对于拷贝构造函数和挪动构造函数,咱们通常心愿它们可能在须要时主动调用,以便进行对象的拷贝和挪动操作。如果将 explicit
利用于拷贝构造函数和挪动构造函数,将会禁止编译器主动调用这些构造函数。
class Foo {
public:
explicit Foo(Foo f) {std::cout << "foo copy" << std::endl;}
};
void test(Foo f);
int main(int argc, char const *argv[]) {
Foo f1;
// error
test(f1);
}
下面例子中,test
函数应用传值形式传递 Foo
对象,在函数调用时,会触发拷贝构造函数,但因为将拷贝构造函数定义为 explicit
,编译器将无奈隐式调用拷贝构造函数。所以会编译失败。
单入参 std::initializer_list
的构造函数
std::initializer_list
是 C++11 中引入的一种非凡类型,用于简化在初始化对象时传递初始化列表的过程。提供了一种简洁的语法来初始化容器、类和其余反对初始化列表的对象。上面是一个简略的应用例子:
class MyClass {
public:
MyClass(std::initializer_list<int> numbers) {// 构造函数的实现}
};
int main() {MyClass obj = {1, 2, 3, 4, 5}; // 应用初始化列表语法进行隐式转换
}
对于带有 std::initializer_list
类型参数的构造函数,也不举荐应用 explicit
关键字。因为应用 std::initializer_list
作为构造函数的入参,就是为了不便初始化对象。如果将 MyClass
的构造函数标记为 explicit
,则在创立obj
对象时,将须要显式地调用构造函数,如MyClass obj({1, 2, 3, 4, 5});
。这样会减少代码的冗余,升高了代码的可读性。
同类型的扩大类
对于有些自定义对象,咱们须要尽量避免它与同类型对象的差别,比方 int
、uint32
、uint64
, 这些类型之间都能互相转换。如果咱们要再定义一个 BigInt
,这个时候,容许BigInt
与那些原生整数类型互相转换是比拟正当的。
小结
- explicit 关键字用于禁止隐式类型转换,在进行类型转换时必须显式地指定转换操作。
- 隐式转换可能导致精度失落、调用指标函数凌乱、对象被谬误回收以及
operator bool
谬误转换等问题。绝大多数状况下,咱们都优先思考禁止隐式转换。 - 在拷贝构造函数和挪动构造函数中,不举荐应用 explicit,以便编译器能够主动调用这些构造函数。
- 对于带有单入参
std::initializer_list
的构造函数,也不举荐应用explicit
,以方便使用初始化列表语法进行隐式转换。 - 同类型的扩大类,为了防止差异化,隐式转换会更适合。
<center> END </center>
【往期举荐】
【重学 C ++】01| C++ 如何进行内存资源管理?
【重学 C ++】02 | 脱离指针陷阱:深入浅出 C++ 智能指针
【重学 C ++】03 | 手撸 C ++ 智能指针实战教程
【重学 C ++】04 | 说透 C ++ 右值援用、挪动语义、完满转发(上)
【重学 C ++】05 | 说透 C ++ 右值援用、挪动语义、完满转发(下)