彻底理解c++的隐式类型转换 (3)

这样的类型转换函数只能用于显式初始化以及特定语境要求的类型转换(比如if里的条件表达式要求返回bool值,这算隐式转换的一种),因此可以避免注释标注的那种语义错误。因此这类转换函数也无法用于其他的隐式转换。

c++11开始函数可以自动推导返回值,模板和自动推到也可以用于自定义的转换函数:

template <typename T> struct SmartPointer { //... T *ptr = nullptr; explicit operator bool() { return ptr != nullptr; } // 配合模板参数 operator T*() { return ptr; } /* 自动推到返回值,与上一个同义 operator auto() { return ptr; } */ }; SmartPointer<int> p = func(); int *p1 = p;

最后用户自定义的转换函数还可以是虚函数,但是只有从基类的引用或指针进行派发的时候才会调用子类实现的转换函数:

struct D; struct B { virtual operator D() = 0; }; struct D : B { operator D() override { return D(); } }; int main() { D obj; D obj2 = obj; // 不调用 D::operator D() B& br = obj; D obj3 = br; // 通过虚派发调用 D::operator D() }

用户定义转换函数不能是类的静态成员函数。

隐式转换序列

了解完标准内置的转换规则和用户自定义的转换规则,我们该看看隐式转换的工作机制了。

对于需要进行隐式转换的上下文,编译器会生成一个隐式转换序列:

零个或一个由标准转换规则组成的标准转换序列,叫做初始标准转换序列

零个或一个由用户自定义的转换规则构成的用户定义转换序列

零个或一个由标准转换规则组成的标准转换序列,叫做第二标准转换序列

对于隐式转换发生在构造函数的参数上时,第二标准转换序列不存在。

初始标准转换序列很好理解,在调用用户自定义转换前先把值的类型处理好,比如加上cv限定符:

struct A {}; struct B { operator A() const; }; const B b; const A &a = b;

初始标准转换序列会把值先转换成适当的形式以供用户转换序列使用,在这里operator A() const希望传进来的this是const B*类型的,而对b直接取地址只能得到B*,正好标准转换规则里有添加底层const的规则,所以适用。

如果值的类型正好,不需要任何预处理,那么初始标准转换序列不会做任何多余的操作。

如果第一步还不能转换出合适的类型,那么就会进入用户定义转换序列。

如果类型是直接初始化,那么只会调用转换构造函数;如果是复制初始化或者引用绑定,那么转换构造函数和用户定义转换函数会根据重载决议确定使用谁。另外如果转换函数不是const限定的,那么在两者都是可行函数时优先选择转换函数,比如operator A();这样的,否则会报错有歧义(GCC 10.2上测试显示有歧义的时候会选择转换构造函数,clang++11.0和标准描述一致)。这也是我们复习了几种初始化有什么区别的原因,因为类的构造形式不同结果也可能会不同。

选择好一个规则后就可以进入下一步了。

如果是在构造函数的参数上,那么隐式转换到此就结束了。除此之外我们需要进行第三部。

第三部是针对用户转换序列处理后的值的类型做一些善后工作。之所以不允许在构造函数的参数上执行这一步是因为防止过度转换后和用户转换规则产生循环。

举个例子:

struct A { operator int() const; }; A a; bool b = a;

在这里a只能转换成int,而为了偷懒我们直接把a隐式转换成bool,问题来了,初始标准转换序列把A*转换成了const A*(作为this,类方法的隐式参数),用户转换序列把const A*转换为了int,int和bool是完全不同的类型,怎么办呢?

这就用上第二标准转换序列了,这里是数值转换,int转成bool。

不过上面只是个例子,请不要这么写,因为在实际代码中会出现问题:

template <typename T> struct SmartPointer { //... T *ptr = nullptr; operator bool() { return ptr != nullptr; } T& operator*() { return *ptr; } }; auto ptr = get_smart_pointer(); if (ptr) { // ptr 是int*的包装,现在我们想取得ptr指向的值 int value = p; // ... }

上面的代码不会有任何编译错误,然而它将引发严重的运行时错误。

为什么呢?因为如注释所说我们想取得指针指向的值,然而我们忘记解引用了!实际上因为要转换成int,隐式转换序列里是这样的:

初始标准转换序列 -----> 当前类型已经调用用户转换序列的要求了,什么都不做

用户定义转换序列 -----> 和int最接近的有转换关系的类型只有bool了,调用这个

第二标准转换序列 -----> 得到了bool,目标的int,正好有规则可用,进行转换

因此你的value只会有两种值,0和1。这就是隐式转换带来的第一个大坑,而上面代码反应出的问题叫做“安全bool(safe bool)”问题。

好在我们可以用explicit把它踢出转换序列:

template <typename T> struct SmartPointer { //... T *ptr = nullptr; explicit operator bool() { return ptr != nullptr; } };

这样当再写出int value = p的时候编译器就能及时发现并报错啦。

第二标准转换序列的本意是帮我们善后,毕竟类的编写者很难面面俱到,然而也正是如此带来了一些坑点。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxssx.html