单自变量 construtors 及隐式类型转换操作符尤其麻烦,因为它们可以在没有任何外在迹象的情况下被调用,这可能导致程序行为难以理解。
另一类问题,诸如 &&
和 ||
等操作符重载后,因为“内置操作符”转移到“用户定制函数”所导致的各种语义敏感变化,容易被忽略。就是手动改了,脑子还是内置操作符的思维。
许多操作符之间有某种标准关系,重载后可能被破坏。
C++ 允许编译器在不同类型之间执行隐式转换(implicit conversions),允许默默地将 char 转换为 int,short 转换为 double。
其实这么转换会遗失信息,如 int 转换为short ,double 转换为 char。
本篇介绍的单自变量 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] 的临时数组作比较。
这种行为不仅没有效率,而且完全没有达到我们的目的。
只要不声明隐式类型转换操作符,就能够避免其所带来的害处。
将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
两个 “>”之间的空格是必要的,如果写成if(a == static_cast< Array
就有了不同的含义。C++编译器将>>
视为单一词元(tokern),会识别为操作符。
允许以一个整数作为 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
,编译器被要求调用 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() 执行上述转换行为。
允许编译器执行隐式类型转换,害处多于好处。不要提供转换函数,除非真的需要它们。