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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 解析隐式类型转换操作operator double() const,带你了解隐式转换危害有多大

前言

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


operator double() const
  {
  return m_num;
  }


隐式类型转换操作符

用法: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;
}


运行结果:

使用注意

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

int main()
{
  Rational r(1, 2);
  cout << r;
  return 0;
}


运行结果:

编译器在发现你没有写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;
}


运行结果:

可以看到程序已经报错了。

正常调用我们写的转换函数就可以正常实现功能。

深思

我们必须明白调用函数转换函数虽然不是很方便,但是可以避免“默认调用那些其实并不打算调用的函数”的错误。越有经验的程序员越要注意避免使用这种类型转换操作符。你要知道STL库中的string容器为什么不实现从string object 到 char* 的隐式转换函数。而是使用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;
}


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

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;
}


arr1数组里面的元素是{2,3,4},arr2数组里面的元素是{0,0,0,0,0}。很明显没有一个元素是相同的,但事实就是打印了三次相同。 但是我想如果你仔细观察这个函数的比较表达式你就会发现,我少写了一个下表运算符。

没错,它应该是lhs[i] == rhs[i]这样的。但是当我意外的漏掉下表运算符时,编译器居然没有任何的抱怨????


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

分析

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


总结

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


解决方案

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);
    }
  }


这样就会提示没有对应的关系运算符。


引入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;
  };


我们将这个proxy classes放到Array的public作用域下即可,然后我们更新单参数的构造函数。

同样编译器将会报错 提示没有对应的关系运算符。


总结

  1. 避免使用隐式类型转换操作符,使用转换函数代替。
  2. 避免使用单参数的构造函数,可以使用explicit关键字和proxy classes(代理类)。
目录
相关文章
|
7月前
|
JavaScript 前端开发 C++
|
5月前
|
SQL 安全 数据库
Ruby on Rails 数据库迁移操作深度解析
【7月更文挑战第19天】Rails 的数据库迁移功能是一个强大的工具,它帮助开发者以版本控制的方式管理数据库结构的变更。通过遵循最佳实践,并合理利用 Rails 提供的迁移命令和方法,我们可以更加高效、安全地管理数据库结构,确保应用的稳定性和可扩展性。
|
6月前
|
存储 算法 搜索推荐
深入解析String数组的操作与性能优化策略
深入解析String数组的操作与性能优化策略
|
5月前
|
存储 数据管理 数据库
CRUD操作实战:从理论到代码实现的全面解析
【7月更文挑战第4天】在软件开发领域,CRUD代表了数据管理的四个基本操作:创建(Create)、读取(Read)、更新(Update)和删除(Delete)。这四个操作构成了大多数应用程序数据交互的核心。本文将深入讲解CRUD概念,并通过一个简单的代码示例,展示如何在实际项目中实现这些操作。我们将使用Python语言结合SQLite数据库来演示,因为它们的轻量级特性和易用性非常适合教学目的。
472 2
|
6月前
|
监控 关系型数据库 分布式数据库
PolarDB时间范围内PCU用量统计:深度解析与操作指南
了解PolarDB云原生数据库的PCU计费至关重要,1PCU相当于1核2GB资源。文章详述如何统计指定时间内PCU用量:登录控制台,查看集群监控,导出数据分析,或使用API接口获取信息。统计结果有助于分析数据库负载、优化资源使用和成本控制。通过对比不同时间段的PCU用量,用户可做出扩展或优化决策。未来,PolarDB有望提供更强大的统计工具。
|
6月前
|
JavaScript 前端开发 开发者
JavaScript中的const关键字解析
JavaScript中的const关键字解析
|
6月前
|
SQL 自然语言处理 前端开发
【JavaScript】ECMAS6(ES6)新特性概览(一):变量声明let与const、箭头函数、模板字面量全面解析
【JavaScript】ECMAS6(ES6)新特性概览(一):变量声明let与const、箭头函数、模板字面量全面解析
50 2
|
5月前
|
存储 算法 搜索推荐
深入解析String数组的操作与性能优化策略
深入解析String数组的操作与性能优化策略
|
6月前
|
Java 数据处理 索引
JAVA中的插入操作:深入解析与实现
JAVA中的插入操作:深入解析与实现
96 1
|
5月前
|
JavaScript
js 解析和操作树 —— 获取树的深度、提取并统计树的所有的节点和叶子节点、添加节点、修改节点、删除节点
js 解析和操作树 —— 获取树的深度、提取并统计树的所有的节点和叶子节点、添加节点、修改节点、删除节点
144 0

推荐镜像

更多
下一篇
DataWorks