• 解析隐式类型转换操作operator double() const,带你了解隐式转换危害有多大


    前言

    我首次看到这种函数的时候是在Flightgear飞行模拟器的源码中。再就是我今天在《More Effective C++》中条款五:对定制的“类型转换函数”保持警觉中看到的。这种函数叫类型转换类型,也是隐式类型转换操作符。

    	operator double() const
    	{
    		return m_num;
    	}
    
    • 1
    • 2
    • 3
    • 4

    隐式类型转换操作符

    用法:operator 类型名 [const]
    注意:这个函数不能指定返回值类型。因为返回值类型在函数名上表现出来了。

    我们看下面这个例子, 有三个隐式转换函数 都是将 Rational 转换为 double

    class Rational
    {
    public:
    	Rational(int numerator = 0, int denominator = 1)
    	{
    		if(0 == denominator)
    		{
    			m_num = 0;
    			return;
    		}
    		m_num = static_cast<double>(numerator) / denominator;
    	}
    	double operator--()
    	{
    		return m_num -= 1;
    	}
    	double operator++()
    	{
    		return m_num += 1;
    	}
    	operator double() const
    	{
    		return m_num;
    	}
    
    private:
    	double m_num;
    };
    
    int main()
    {
    	Rational r(1, 2);
    	double d = 0.5 * r; // 将r转换为double 然后执行乘法运算。
    	cout << d << endl;
    	cout << ++d << "\t" << d << endl;
    	cout << d++ << "\t" << d << endl;
    	cout << --d << "\t" << d << endl;
    	cout << d-- << "\t" << d << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    运行结果:
    在这里插入图片描述

    使用注意

    仔细看下面这段代码,cout << r; 我们前面并没有重写operator<<函数。但是下面的代码可以正常运行。

    int main()
    {
    	Rational r(1, 2);
    	cout << r;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    运行结果:
    在这里插入图片描述
    编译器在发现你没有写operator<<函数时,编译器会想尽办法去找一系列可接受的隐式类型转换 让函数执行起来。
    这将会导致程序出现意想不到的结果。

    解决方案

    我们以功能对等的另一个函数取代类型转换操作符。我们可以自己写个asDouble的函数代替operator double()

    看下面这段代码:

    class Rational
    {
    public:
    	Rational(int numerator = 0, int denominator = 1)
    	{
    		if(0 == denominator)
    		{
    			m_num = 0;
    			return;
    		}
    		m_num = static_cast<double>(numerator) / denominator;
    	}
    	//operator double() const
    	//{
    	//	return m_num;
    	//}
    
    	double asDouble() const
    	{
    		return m_num;
    	}
    
    private:
    	double m_num;
    };
    
    int main()
    {
    	Rational r(1, 2);
    	cout << r;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    运行结果:
    可以看到程序已经报错了。
    在这里插入图片描述
    正常调用我们写的转换函数就可以正常实现功能。
    在这里插入图片描述

    深思

    我们必须明白调用函数转换函数虽然不是很方便,但是可以避免“默认调用那些其实并不打算调用的函数”的错误。越有经验的程序员越要注意避免使用这种类型转换操作符。你要知道STL库中的string容器为什么不实现从string objectchar* 的隐式转换函数。而是使用c_str的成员函数吗?要知道他们都是非常老练的程序员了吧。

    构造函数造成的隐式转换

    单参数的构造函数也能实现隐式转换,而且难为发现。这可比隐式转换操作符难处理多了。
    下面实现了一个模板数组类Array,并提供了俩种初始化方式、元素个数、下标运算符等。还实现了Array类的关系运算符(注:该函数仅仅为了测试效果,此处不谈函数功能是否正确)

    template<class T>
    class Array
    {
    public:
    	Array(int lowBound, int highBound)
    	{
    		if(lowBound > highBound)
    		{
    			return;
    		}
    		m_arr.resize(highBound - lowBound);
    		for (size_t i = lowBound; i <= highBound; ++i)
    		{
    			m_arr[i - lowBound] = i;
    		}
    	}
    	Array(int size)
    	{
    		if(size > 0)
    		{
    			m_arr.resize(size);
    		}
    	}
    	size_t size() const
    	{
    		return m_arr.size();
    	}
    	const T& operator[](size_t index) const
    	{
    		return m_arr[index];
    	}
    
    	T& operator[](size_t index)
    	{
    		return m_arr[index];
    	}
    
    private:
    	vector<T> m_arr;
    };
    
    bool operator==(const Array<int>& lhs, const Array<int>& rhs)
    {
    	for (size_t i = 0; i < lhs.size() && i < rhs.size(); ++i)
    	{
    		if(lhs == rhs[i])
    		{
    			cout << "相同" << endl;
    		}
    		else
    		{
    			cout << "不相同" << endl;
    			
    		}
    	}
    	
    	return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    上述代码看着挺正常的吧,但是下面的测试代码和测试结果你绝对会吃惊。

    int main()
    {
    	Array<int> arr1(2, 4);
    	Array<int> arr2(5);
    	for(size_t i = 0; i < arr1.size(); ++i)
    	{
    		cout << arr1[i] << "\t";
    	}
    	cout << endl;
    	for (size_t i = 0; i < arr2.size(); ++i)
    	{
    		cout << arr2[i] << "\t";
    	}
    	cout << endl;
    
    	arr1 == arr2;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    arr1数组里面的元素是{2,3,4},arr2数组里面的元素是{0,0,0,0,0}。很明显没有一个元素是相同的,但事实就是打印了三次相同。 但是我想如果你仔细观察这个函数的比较表达式你就会发现,我少写了一个下表运算符。
    在这里插入图片描述
    没错,它应该是lhs[i] == rhs[i]这样的。但是当我意外的漏掉下表运算符时,编译器居然没有任何的抱怨????

    如果你的编译器足够智能那么你会发现有这么一句提醒 使用构造函数array (int size) ,用户自定义将rhs[i]int 类型转换为’array 类型
    在这里插入图片描述

    分析

    编译器没有找到对于版本的关系运算符,但是编译器只需要调用Array的构造函数(需要一个int变量),就可以将int转换为Array 类型。所有就变成了lhs == Array(rhs[i])。姑且抛开这个错误不谈,在这个循环中每一个的进行比较都会产生和释放一个临时的Array对象 效率极低。

    总结

    虽然我们不声明隐式类型转换操作符,就可以避免一些不必要的一些麻烦,但是单变量的构造函数却防不胜防,因为你极有可能要为用户提供这个一个功能,但你又不想让编译器不分青红皂白的调用这个构造函数,

    解决方案

    explicit关键字

    使用explicit关键字修饰 关闭函数的类型自动转换(防止隐式转换) 用法简单。

    	explicit Array(int lowBound, int highBound)
    	{
    		if(lowBound > highBound)
    		{
    			return;
    		}
    		m_arr.resize(highBound - lowBound + 1);
    		for (size_t i = lowBound; i <= highBound; ++i)
    		{
    			m_arr[i - lowBound] = i;
    		}
    	}
    	explicit Array(int size)
    	{
    		if(size > 0)
    		{
    			m_arr.resize(size);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这样就会提示没有对应的关系运算符。
    在这里插入图片描述

    引入Proxy classes 代理类

    为了避免int类型变量的隐式类型转换,我们可以将int类型的变量封装为一个proxy classes。用新类来保存要被产生数组的大小。

    class ArraySize
    	{
    	public:
    		ArraySize(size_t numElements) :m_size(numElements){}
    		size_t size() const
    		{
    			return m_size;
    		}
    
    	private:
    		size_t m_size;
    	};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们将这个proxy classes放到Array的public作用域下即可,然后我们更新单参数的构造函数。
    在这里插入图片描述

    同样编译器将会报错 提示没有对应的关系运算符。
    在这里插入图片描述

    总结

    1. 避免使用隐式类型转换操作符,使用转换函数代替。
    2. 避免使用单参数的构造函数,可以使用explicit关键字和proxy classes(代理类)。
  • 相关阅读:
    第17章 注销器的泛型实现
    Splay
    代码随想录算法训练营Day60 | 单调栈(3/3) LeetCode 84.柱状图中最大的矩形
    编译原理——构造预测分析表(判断某字符串是否是文法G(E)的句子)
    Kotlin基础入门 - 创建、兼容一个属于自己的Kotlin项目
    一、solidity 基础《实战NFT web3 solidity(新版本0.8.+)》
    4.1 数据仓库基础与Apache Hive入门
    java中的并发工具类
    中国互联网众筹行业
    PySpark特征工程(I)--数据预处理
  • 原文地址:https://blog.csdn.net/qq_45254369/article/details/126167367