MEC | 条款5 对定制的“类型转换函数”保持警觉

MEC | 条款5 对定制的“类型转换函数”保持警觉

MEC | 条款5 对定制的“类型转换函数”保持警觉_第1张图片

文章目录

  • MEC | 条款5 对定制的“类型转换函数”保持警觉
    • 单自变量 construtors
      • 定义
      • 问题
      • 解决办法
        • 1. 关键词 explicit
        • 2. proxy classes
    • 隐式类型转换操作符
      • 定义
      • 问题
      • 解决
    • 结论
      • >>>>> 欢迎关注公众号【三戒纪元】 <<<<<

对于可以重载操作符(overloadable operators),很多人会觉得非常方便,可以定制化,多么自由。自由总会付出沉重的代价。

  • 单自变量 construtors 及隐式类型转换操作符尤其麻烦,因为它们可以在没有任何外在迹象的情况下被调用,这可能导致程序行为难以理解。

  • 另一类问题,诸如 &&||等操作符重载后,因为“内置操作符”转移到“用户定制函数”所导致的各种语义敏感变化,容易被忽略。就是手动改了,脑子还是内置操作符的思维。

  • 许多操作符之间有某种标准关系,重载后可能被破坏。

C++ 允许编译器在不同类型之间执行隐式转换(implicit conversions),允许默默地将 char 转换为 int,short 转换为 double。

其实这么转换会遗失信息,如 int 转换为short ,double 转换为 char。

本篇介绍的单自变量 construtors隐式类型转换操作符 是编译器允许的,但是会带来很多不必要的转换

单自变量 construtors

定义

单自变量 construtors :能够以单一自变量成功调用的 constructors

如此 constructors 可能声明拥有单一参数,也可能声明拥有多个参数,并且除了第1个参数之外都有默认值

class Name
{
public:
    Name(const string& s); // 可以把 string 转换为 Name
    ...
};

class Rational {
 public:
  Rational(int numerator = 0, int denominator = 1); // 可以把 int 转换为 Rational
}

问题

单自变量 construtors 完成的隐式转换,较难消除。

举个例子,写一个针对数组结构的 class template

template<class T>
class Array {
public:
    Array(int lowBound, int highBound); // 指定索引的上限和下限
    Array(int size);

    T& operator[] (int index);
    bool operator==(const Array<int>& lhs, const Array<int>& rhs);
    ...
};

第1个 constructor 允许指定某个范围的数组索引,作为双自变量 constructor ,此函数没有资格成为类型转换函数;

第2个 constructor 允许用户指定数组袁术的个数,可以被用来作为1个类型转换函数,结果导致无尽苦恼。

比如要比较两个数组是否相同:

Array<int> a[10];
Array<int> b[10];
...
for (int i = 0; i < 10; i++)
{
    if(a == b[i]) {
        // work
    } else {
        // work
    }
}

这里 第6行因为错误地将 a[i] 写成了 a,但是编译器会调用 Array 第2个constructor 可以将 int 转换为 Array object。

于是编译器会产生这样的代码:

for (int i = 0; i < 10; i++)
{
    if(a == static_cast< Array<int> >(b[i])) { 
     ...

于是每次循环都会拿 a 的内容和 一个大小为 b[i] 的临时数组作比较。

这种行为不仅没有效率,而且完全没有达到我们的目的。

解决办法

只要不声明隐式类型转换操作符,就能够避免其所带来的害处。

1. 关键词 explicit

将constructor 声明为 explicit, 编译器便不能因为隐式类型转换而调用它们,显示转换仍是可以的。

template<class T>
class Array {
public:
    ...
    explicit Array(int size); // 注意使用“explicit”
    ...
};


Array<int> a(10); // good. explicit ctor 可以像往常一样作为对象构造函数之用

Array<int> b(10); // good

if (a == b[i]) ... // wrong!无法将int 转换为 Array 

if (a == Array<int>(b[i])) ... // 没问题, 将 int 转换为 Array 是一种显示行为,该代码逻辑让人质疑

if (a == static_cast< Array<int> >(b[i])) ... // 没问题,同质疑

if (a == (Array<int>)b[i]) ... // C 旧式转型,同质疑

上述if(a == static_cast< Array >(b[i])) {两个 “>”之间的空格是必要的,如果写成if(a == static_cast< Array>(b[i])) 就有了不同的含义。C++编译器将>>视为单一词元(tokern),会识别为操作符。

2. proxy classes

允许以一个整数作为 constructor 自变量来指定数组大小,又能组织一个整数被隐式转换为一个临时性Array 对象

首先产生一个新的class ,名为 ArraySize。只有1个目的,ArraySize用来表现即将被产生的数组的大小

template <class T>
class Array {
 public:
  class ArraySize {
   public:
    ArraySize(int numElements) : theSize(numElements) {}
    int size() const { return theSize; }

   private:
    int theSize;
  };
  Array(int lowBound, int highBound);  // 指定索引的上限和下限
  Array(int size);
};

这里把ArraySize 嵌套进 Array 内,强调它永远与Array 配套使用。

ArraySize 在 Array 的public 区,任何人也都能访问它。

  • 现在执行Array a(10);,编译器被要求调用 Array class 中的一个 自变量为 int 的constructor,但是其实不存在这样的 constructor。但编译器能够将 int 自变量转换为一个临时的ArraySize 对象,而这正是 Array constructor 需要的,于是函数调用得以成功

  • 再来看看下面这段代码

  for (int i = 0; i < 10; i++)
  {
      if(a == static_cast< Array<int> >(b[i])) { 
       ...

编译器需要一个类型为 Array 对象在“==”两边,但是此时没有“单一自变量,类型为int”的constructor。

编译器不能考虑将 int 转换为1个临时的 ArraySize 对象,再将临时变量转化为 Array 对象。这将调用2个用户定制转换行为,如此转换程序是禁止的。

类似 ArraySize 的类成为 proxy classes,因为它的额每一个对象都是为了其他对象而存在,像其他对象的代理(proxy)一样。

隐式类型转换操作符

定义

隐式类型转换操作符 : 是一个 member function,关键词 operator 之后加上1个类型名称。

因为已经有了类型名称,因此不能为此函数指定返回值类型。

class Rational {
 public:
  ...
  operator double() const; // 将 Rational 转换为 double
}

可能被自动调用:

Rational randy(1,2); // r 的值为 1/2
double d = 0.5 * r;  // 将r 转换为double,然后执行乘法运算。

问题

可是当我们想要输出 隐式类型转换操作符的时候,就会有问题:

Rational r(1, 2); // good, 输出 “1/2”
std::cout << r;   // 输出double ,而非分数。调用double()

虽然没有为Rational 重载操作符 << ,但是编译器会调用 Rational::operator double,将 r 转换为 double,保证第2行的正确输出,输出double ,而非分数。

虽然也能保证程序运行,但是造成了错误(非预期)的函数调用

解决

以功能对等的另一个函数取代类型转换操作符

可以设计1个名为 asDouble 的成员函数取代 operator double,允许将Rational 转换为 double

class Rational {
 public:
  ...
  double asDouble() const; // 将 Rational 转换为 double
}

这样的member function 必须被明确调用:

Rational r(1,2);
std::cout << r;  // 错误! Rationals 没有 operation <<;
std::cout << r.asDouble(); // good。以double的形式输出 r

大部分时候,调用类型转换函数带来些许不便,但是因为不再默默调用那些其实并不打算调用的函数而获得弥补。

这就是为什么 标注库中的 string 类型并未含有“从 string object 至 C-style char* ”的隐式转换函数的原因

标准库提供了一个显示的 member function c_str() 执行上述转换行为。

结论

允许编译器执行隐式类型转换,害处多于好处。不要提供转换函数,除非真的需要它们。


>>>>> 欢迎关注公众号【三戒纪元】 <<<<<

你可能感兴趣的:(C++,c++)