c++ 类型转换与explicit

Posted zkccpro

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++ 类型转换与explicit相关的知识,希望对你有一定的参考价值。

c++ 类型转换与explicit

本文试图搞清楚这样2个问题:

  • c++ 类型转换有哪些?作用分别是什么?实际工程中应该怎么用?
  • 经常查看标准库源码的同学会发现标准库中很多类构造函数会带有explicit声明,explicit关键字是为了做什么的?

一、c++的类型转换简介

可不要上来就说4种类型转换昂。。类型转换按照触发条件分可分为:隐式类型转换,显式类型转换;而按照行为分可分为:static,dynamic,const,reinterpret。我们先按照行为划分介绍4种类型转换:

1. static_cast

静态转换是最常用的一种转换了!作用效果与c风格的强制类型转换写法一样。另外,下文会提及:所有隐式转换默认使用static_cast。

int a=2;
double b1=static_cast<double>(a);//c++
double b2=(double)a;//c

但static_cast在下行转换时可能会使代码不安全(未定义行为):

class base
public:
    virtual void func()cout<<"this is base!"<<endl;
;
class sub:public base
public:
    virtual void func()overridecout<<"this is sub!"<<endl;
;

int main()
    base* b=new base;//父类指针指向父类对象
    sub* s=static_cast<sub*>(b);//下行转换用了static_cast
    cout<<s<<endl;//转换成功,指针不为nullptr
    s->func();//输出:this is base!

可以发现,用static_cast把父类转换成子类之后,对其调用虚函数仍然是父类的版本!可见static_cast对父类

所以一个常见的建议是:涉及子类和父类之间的转换不要用static_cast就完事了!

2. dynamic_cast

动态转换就会“动态地”分析转换前后的类型,是否会导致上面这种未定义行为,如果可能出现,则直接转换失败。接下来我们详细谈谈这个:

首先,dynamic_cast是有语法上的使用条件的:

  • 必须用于带有虚函数的继承体系
  • 转换目标类型和待转换类型必须是指针或引用

知道了使用条件,接下来看看dynamic_cast的行为:

class base
public:
    virtual void func()cout<<"this is base!"<<endl;
;
class sub:public base
public:
    virtual void func()overridecout<<"this is sub!"<<endl;
;

int main()
    base* b=new base;//父类指针指向父类对象
    sub* s=dynamic_cast<sub*>(b);// 下行转换
    cout<<s<<endl;//输出:0,转换失败

可见,为了避免static_cast造成【经转换后的子类无法调用自己的虚函数版本】的问题,dynamic_cast的作用就在于直接阻止这种情况的出现,以免你的代码出现“难以发现”的错误。

另外,如果基类不含虚函数时,使用dynamic_cast转换会不过编译:【source type is not polymorphic,基类类型并非多态】

class base
public:
    int a;
;

class sub:public base
public:
    int b;
;
int main()
    base* b=new base;
    sub* s=dynamic_cast<sub*>(b);// 编译出错

3. const_cast

//去除指针的底层const属性
	int aa=2;
    const int* a=&aa;//底层const
    int* b=const_cast<int*>(a);
    *a=3;//a指向内容不能改,底层const,编译出错!
    *b=3;//转换之后就可以了
    cout<<*b<<endl;

//去除指针的顶层const属性
    int aa=2;
    int bb=3;
    int* const a=&aa;//顶层const
    int* b=const_cast<int*>(a);
    a=&bb;//a的指向不能改,顶层const,编译出错!
    b=&bb;//转换之后就可以了
    cout<<*b<<endl;

说到这里,就不得不提一下我对顶层/底层const的一些新的理解:

首先是写法,初学者可能都会觉得顶层底层的写法难以记住,容易混淆:

const int* a;//1. 底层
int* const b;//2. 顶层
int const *c;//3. 底层

上面3种都是比较常见的写法,你能快速反应出来吗?一开始我也迷糊,特别是看到第3种写法。。。

我是怎么记的呢?const放在 * 后面就是修饰指针的,即为顶层,其他都是底层,即修饰变量类型(int)的。

而对于底层const,一般理解是:不能修改指向的内容,这句话没错,但不准确,看下面的代码:

int a = 1;
int const *p_a = &a; 
*p_a = 2;//底层const指向的区域不能由此底层const指针修改,编译出错
a=2;//但并不意味着此段内存中的值不能改,只是不能通过此底层const指针修改!

上述代码在GNU下测试过,所以,对于底层const,我们的结论是:

底层const的本质是规定一段地址,这段地址中的值不能由此底层const指针修改,但却能通过其他方式修改。

4. reinterpret_cast

噢。。这个东西真的是“潘多拉魔盒”!为什么这么说呢?因为它帮你跨过c++帮你苦心经营的“抽象”,给你提供一个直接和底层的汇编语言打交道的机会!

在之后的章节我会详细介绍汇编语言提供怎样的机制,来访问不同类型指针指向的内存。现在我们先简单“尝尝鲜”:

int aa=2;
int* a=&aa;
char* c=reinterpret_cast<char*>(a);
int i=0;
while(i<sizeof(int))
    cout<<static_cast<int>(*(c++))<<" ";
    i++;

cout<<endl;

第一次接触reinterpret_cast看上面的代码可能会觉得头皮发麻,比如说我。。。实际上,上面的代码意味着:

将一段存储int类型的内存空间,按照char类型的方法来解析之!

啥意思呢?仔细观察你会发现:c指针每次++,起始地址只向前移动了1B,而int类型的指针每次++应该移动4B才对!这就说明对一段地址位级别的解释发生了变化。

上段代码GNU下的输出:

zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_cast.cc -o test_cast && ./test_cast
2 0 0 0

它直接输出了存储4字节int类型变量地址中,每1字节的存储的值,具体输出的内容与整数机内表示有关,我们之后的文章也会详细介绍。

说完了reinterpret_cast的行为,那它有什么用呢?我们什么时候非得跨国c/c++的抽象来直接改变底层地址的解析性质呢?

恐怕一个经典的例子就是buffer设计。当你的buffer需要存储不同类型的值时,将他们存入buffer前都reinterpret_cast<char*>,再放入一个char*类型的buffer;当你自己动手设计网络消息中间件的buffer时,你需要统一按字节(char类型)将各种信息打出去,这或许也需要reinterpret_cast<char*>

但必须要提醒的是,用了这个东西,也就意味着没有完全遵守c++2.0规范,因为直接修改了底层的字节解释,如果你对你程序的行为不敢100%保证它可以按你的想法去改变解析的地址内容(特别是网络buffer,因为网络复杂的情况很可能导致你的buffer出现一些时序对不上的情况,除非你考虑了所有可能发生的时序问题并做好了正确的处理,否则在网络buffer中用reinterpret_cast<char*>属于“找死”行为。

二、 explicit关键字的使用场景

当你通读过《c++ primer》后,你会了解到,explicit一般用于修饰类的构造函数,阻止类调用此构造函数发生隐式转换。但你了解到此你可能还是不知道到底什么时候该用explicit修饰构造函数。。(反正我是这样)

幸亏看了侯捷老师的视频,帮我搞清楚了explicit的使用场景究竟是什么,我在GNU下复现了侯捷老师给的例子:

class fraction
public:
    fraction()=default;
    fraction(int num,int den=1)//有参构造,允许隐式转换
    :num_(num),den_(den)
    //自定义statis_cast的规则,一个类重载了这个就可以向目标类型隐式转换/静态转换了
    operator double()const
        return static_cast<double>(num_/den_);
    
    fraction operator+(const fraction& f)
        //应该按分数运算的规则返回,但在这里这不重要
        return fraction();
    
private:
    int num_0;
    int den_1;
;
int main()
    fraction f(3,5);
    double d=f+3;//ambiguous

上面这段代码会编译报错吗?答案是 会的!但是为啥呢。。

原因在于主函数的最后一行,表达式f+3.0对编译器来说有2种执行方法:

  1. 由于你重载了double(),所以可以把 f 隐式转换成double,3转换成double,然后相加,没毛病。
  2. 由于你没有阻止有参构造的隐式转换,而且整数3正好可以匹配你的有参构造(有默认参数,可以接受一个参数),所以编译器可以把 3 隐式转换成 fraction,然后两个fraction正好又有相加规则,最后再把相加后的fraction隐式转换成double,赋值给d。稍麻烦点,但也没毛病啊!

至此,编译器有了2种执行方法,且都可以走通,这种情况会导致编译出错,这是GNU下的编译输出:

zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_cast.cc -o test_cast && ./test_cast
test_cast.cc: In function ‘int main()’:
test_cast.cc:67:15: error: ambiguous overload for ‘operator+’ (operand types are ‘fraction’ and ‘double’)
   67 |     double d=f+3.0;
      |              ~^~~~
      |              | |
      |              | double
      |              fraction
test_cast.cc:67:15: note: candidate: ‘operator+(double, double)<built-in>
   67 |     double d=f+3.0;
      |              ~^~~~
test_cast.cc:24:14: note: candidate: ‘fraction fraction::operator+(const fraction&)24 |     fraction operator+(const fraction& f)
      |              ^~~~~~~~

那如何避免这种ambiguous呢?很简单,只要阻止一条路径就行了,下面的方法都在GNU下经过测试了

  • 阻止路径1:把operator double()重载去掉
  • 阻止路径2:给有参构造加上explicit修饰:
explicit fraction(int num,int den=1)//阻止隐式转换,explicit修饰要写在前面
:num_(num),den_(den)

避免这种ambiguous的出现恰好是explicit关键字最常用的用途了,掌握这个即可!

以上是关于c++ 类型转换与explicit的主要内容,如果未能解决你的问题,请参考以下文章

c++ 类型转换与explicit

C++中explicit关键字的使用

(译)What does explicit keyword mean?

explicit:C++规定,当定义了只有一个参数的构造函数时,同时也定义了一种隐式的类型转换

C++中的explicit关键字的用法

explicit 关键字 c++