在 C++ 中,转换构造函数 与 类型转换运算符 共同构成了 类类型转换 ,也叫 用户定义的类型转换
类型转换运算符 是类的特殊转换函数,可以将类的对象转换为其他类型
operator type()
其中,
operator type() const
从声明来看,可以发现:
以下是相对应的错误示例
struct SmallInt
{
SmallInt(int val = 0)
{
if(val < 0 || val > 255)
throw std::out_of_range("Bad SmallInt");
this->val = val;
}
int operator int() const {} // 错误,不能对转换函数指定返回类型
operator int(int) const {} // 错误,用户定义的转换中不允许使用参数
operator int*() const
{
return 42; // 错误,返回值类型与函数类型不匹配
}
private:
std::size_t val;
};
此外,由于类型转换一般不需要改变对象的值,因此,类型转换函数一般被定义为 const 的
struct SmallInt
{
SmallInt(int val = 0)
{
if(val < 0 || val > 255)
throw std::out_of_range("Bad SmallInt");
this->val = val;
}
// 隐式定义的类类型转换运算符
operator int() const
{
return val;
}
private:
std::size_t val;
};
void test(void)
{
SmallInt a;
a = 10;
int b = a + 10;
}
在上面的示例中:
a = 10
先调用 转换构造函数 ,将 10 转换为 SmallInt 对象,然后,再调用 operator==
完成赋值操作int b = a + 10;
调用 类类型转换运算符 ,将 a 转换为 int,然后完成剩下的操作此外,尽管 一次只能执行一个用户定义的类型转换,但是可以和内置数据类型结合使用
也就是说,可以先将 SmallInt 对象转换为 int,再使用内置类型转换将 int 转换为其他类型
同样的,可以将某一数据类型(如 double )转换为 int,再使用转换构造函数转换为 SmallInt
例如:
SmallInt a = 3.14; // 调用转换构造函数
double b = a + 3.14; // 将 a 转换为 int,再将 int 转换为 double
如果类类型与转换类型不存在明显的映射关系,那么,定义这样的类型转化运算符可能是具有误导性的
因此,建议在这种情况定义普通的成员函数来满足需求,而不是定义类型转换运算符
在实际编码中,没有特殊需求,一般不会提供类型转换运算符,然而,定义向 bool 类型的转换还是比较常见的
对于 bool 类型来说,它是一种算术类型,因此,如果类对象可以被转换为 bool 类型,那它就可以被用于任何算术表达式中
例如,下面的代码:
int i = 3;
std::cin << i;
由于 istream 没有定义 <<
运算符,因此,这段代码应该产生错误
然而,如果 std::cin
可以被转换为 bool 类型(实际上不会,因为 向 bool 类型的转换是被显式定义的,下文有所提及)那么,这个 bool 值将会被进一步提升至 int 类型,并作为 <<
运算符的左侧运算对象(0 或者 1),最终,该 bool 值被左移 i 个位置
为了解决上述代码可能带来的问题,新标准引入了 显式定义的类型转换运算符 来解决这个问题
struct SmallInt
{
SmallInt(int val = 0)
{
if(val < 0 || val > 255)
throw std::out_of_range("Bad SmallInt");
this->val = val;
}
// 显式定义的类类型转换运算符
explicit operator int() const
{
return val;
}
private:
std::size_t val;
};
void test(void)
{
SmallInt a = 10; // 正确,a 的构造函数不是显式定义的
int b = a + 3; // 错误,a 的类型转换运算符是显式定义的
int c = static_cast<int>(a) + 3; // 正确,显式要求类型转换
}
当类型转换运算符是显式的,若需要类型转换,需要显式要求
当类型转换被用做条件,上述的规则无效,也就是说,显式的类型转换会被隐式执行
例如:
struct SmallInt
{
SmallInt(int val = 0);
explicit operator int() const;
// 显式定义的类类型转换运算符
explicit operator bool() const;
private:
std::size_t val;
};
void test0(void)
{
SmallInt a = 20;
if(a) // if statement
std::cout << static_cast<int>(a) << std::endl;
while (a) // while
break;
for(int i = 0; a; ++i) // for
break;
int b = a ? static_cast<int>(a) : 0; // 三元运算符
}
上述代码能够正常执行
根据上面的示例可以看出,将 转换为 bool 类型的类型转换运算符 定义为 explicit 的,是较为恰当的做法,能避免很多问题,因此,应当将 转换为 bool 类型的类型转换运算符 定义为 explicit 的
如果一个类内部有多个类型转换,需要 保证类类型到目标类型只有一种转换方式,否则会出现二义性
常见的情况有:
看起来有点抽象,上代码
struct B;
struct A
{
int val;
A(int val);
A(const B &b); // 将 B 转换为 A
};
struct B
{
int val;
B(int val)
:val(val){}
operator A() const // 将 B 转换为 A
{
return A(val);
}
};
A func(const A &a)
{
return a;
}
void test(void)
{
B b = 10;
A a = func(b); // 二义性调用
}
A::A(int val)
:val(val){}
A::A(const B &b)
:val(b.val){}
在 A a = func(b);
这句中,因为同时存在两种将 b 转换为 A 的方法
因此,对 func 的调用是有二义性的
同样的,对于如下代码:
struct A
{
int val;
A() = default;
operator double() const
{return static_cast<double>(val);}
operator int() const
{return val;}
};
void func(long double lg){}
void test()
{
A a;
func(a); // 二义性调用
}
在对 func 的调用中,无论是哪种类型转换,均不能精确匹配 func,因此,调用 operator double()
不比调用 operator int()
更好,最终产生二义性调用
struct A
{
int val;
A() = default;
A(int);
A(double);
};
void test()
{
long double lg = 10.0;
A a(lg); //二义性调用
}
这也是一样的道理
上面的两个例子之所以会产生二义性,根本原因是:它们所需的标准类型转换的级别一致
因此,当我们使用类类型转换时,如果转换过程 包含标准类型转换,那么,标准类型转换 决定了最佳匹配的过程
short s = 1;
A a(s); // 调用 A::A(int);
一般来说,有两种方式避免二义性的产生
不要让两个类执行相同的类型转换(上面的 A 、B 类就是很好的例子)
避免转换为内置算术类型,如果已经定义了一个转换为内置算术类型的转换,那么:
struct A
{
int val;
A() = default;
A(int val)
{
this->val = val;
};
operator int() const
{
return val;
}
// 接下来不要定义这个类型转换!!!
// 让标准类型转换帮你完成!!!
// operator double() const
// {
// return static_cast (val);
// }
// 不要定义这个运算符,让 A 转换为 int
// 再使用内置运算符
int operator+(int a)
{
return val + a;
}
};
void test(void)
{
A a, a1;
long double b = a; // 如果定义了 operator double() const,将会产生二义性调用
int i = a + 1; // 如果定义了 int operator+(int a),可能与内置算术产生冲突
}
当某几个重载的函数的形参分属与不同的类类型,并且,这些类都 定义了相同的转换构造函数,那么,将会产生二义性调用问题
举个例子:
struct A
{
A() = default;
A(int);
};
struct B
{
B() = default;
B(int);
};
void func(A a);
void func(B b);
void test(void)
{
func(10); // 错误,调用 A::A(int) 还是 B::B(int) ?
}
当然,可以通过显式构造来消除这种二义性,不过,这通常意味着程序设计存在缺陷
func(A(10));
再看这个例子:
struct A; // 与上面的定义一致
struct B
{
B() = default;
B(double); // 不同点:double => B
};
void func(A a);
void func(B b);
void test(void)
{
func(10); // 错误,调用 A::A(int) 还是 B::B(double) ?
}
可以发现,即使 B 的转换构造函数接受一个 double,我们向 func 传入一个 int 类型的参数,仍然产生二义性调用
因此,可以得出这样的结论:即使其中一个调用需要额外的标准类型转换,并且另一个不用(也就是精确匹配),也会产生二义性调用
表达式的运算符的候选函数包括成员函数与非成员函数
举个例子
B operator+(const B &b0, const B &b1);
struct B
{
int val;
B() = default;
B(int);
operator int();
};
void test1(void)
{
B b0, b1;
B b2 = b0 + b1; // 正确,调用 B operator+(const B &b0, const B &b1);
B b3 = b0 + 1; // 错误,与内置加法运算冲突
}
在 B b3 = b0 + 1;
中的 ‘+’,候选函数就有两个
因为可以将 1 转换为 B,调用 B operator+(const B &b0, const B &b1);
当然,也可以将 B 转换为 int,调用 内置运算符 “arithmetic + arithmetic”
因此,有如下结论:
SmallInt operator+(const SmallInt &a, const SmallInt &b);
struct SmallInt
{
int val;
SmallInt() = default;
SmallInt(int);
operator int() const;
};
LongDouble operator+(const LongDouble&, double);
struct LongDouble
{
LongDouble operator+(const SmallInt&);
long double val;
LongDouble() = default;
LongDouble(double);
operator double();
operator float();
};
void test(void)
{
SmallInt si;
LongDouble ld;
ld = si + ld; // 错误
ld = ld + si;
}
先来分析 ld = si + ld
SmallInt operator+(const SmallInt &a, const SmallInt &b);
,无法将 LongDouble 转化为 SmallInt,因为一次只允许进行一次用户定义的类型转换LongDouble operator+(const LongDouble&, double);
,无法将 SmallInt 转化为 LongDouble,因为一次只允许进行一次用户定义的类型转换LongDouble operator+(const SmallInt&);
,无法将 LongDouble 转化为 SmallInt,也无法将 SmallInt 转化为 LongDouble因此,我们定义的重载运算符均无法调用,唯一的做法就是调用内置运算符了
再来分析 ld = ld + si
LongDouble operator+(const SmallInt&);
,实参与形参精确匹配,可以调用LongDouble operator+(const LongDouble&, double);
,需要将 SmallInt 转换为 int,再将 int 转换为 double,相较于成员函数 LongDouble operator+(const SmallInt&);
的优先级低因此,最终,ld = ld + si
将正确调用成员函数 LongDouble operator+(const SmallInt&);