乐趣区

关于后端:重学-C06-C该不该使用-explicit

文章首发

【重学 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});
}

下面的 i4i5i6 以及前面的 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;}

这段代码,咱们本意是想要判断 foo1foo2 是否相等,但少写了一个 =, 因为 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});。这样会减少代码的冗余,升高了代码的可读性。

同类型的扩大类

对于有些自定义对象,咱们须要尽量避免它与同类型对象的差别,比方 intuint32uint64, 这些类型之间都能互相转换。如果咱们要再定义一个 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 ++ 右值援用、挪动语义、完满转发(下)

退出移动版