🫑重载、类型转换与运算符
2022-6-5
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
explicit 的具有一个参数的构造函数定义了隐式转换。这种构造函数将参数类型的对象转换到类类型对象。我们还可以定义从类类型到别的类型的转换。我们通过定义一个转换操作符来定义从类类型的转换。转换构造函数和转换操作符定义类类型转换。这些转换也被称为是用户定义的类型转换
 

类型转换操作符

类型转换操作符(conversion operator)是一种特殊的成员函数,可以将一个类类型的值转为一个其它类型的值。转换函数通常由这样一种通用的形式:
其中 type 表示一个类型。转换操作符可以为任何可以被函数返回的类型(除了 void)定义。转换成数组或者函数类型是被禁止的。转换成指针类型(数据和函数指针)以及引用类型是允许的。
转换操作符没有显式说明返回值类型并且没有参数,它们必须被定义为成员函数。转换操作符通常不应该改变发生转换的对象的状态。因而,转换操作符通常被定义为 const 成员。
注:类型转换函数必须是成员函数,并且不指定返回值类型,其参数列表必须是空的。这个函数通常应该是 const 的。
 

定义具有类型转换操作符的类

定义一个比较简单的类,令其表示0到255之间的一个整数
其中构造函数将算术类型转为SmallInt,转换操作符将SmallInt转为int
尽管编译器一次只会执行一个用户定义的转换,隐式用户定义的转换在内置标准转换之前或之后发生。因而,可以传递任何算术类型给 SmallInt 的构造函数。通常,可以使用转换操作符将 SmallInt 转为int然后将结果int值转为别的算术类型:
由于类型转换操作符是隐式运用的,没有任何途径去传递参数给这些函数。因而,类型转换操作符不能被定义为接收参数。尽管类型转换函数没有指定其返回值类型,每个类型转换函数都必须返回一个与其名字所表示的类型一样的值。
注意:避免滥用转换函数
 
与使用重载操作符一样,谨慎地使用转换操作符可以极大地简化类设计者的工作,并且使得类更加容易使用。然而,一些转换是容易被误导的。当没有明显的在类类型与要转换的类型之间的单一映射,转换操作符将是误导的。
比如,考虑表示日期的 Date 类,我们可能会认为在 Date 和 int之间提供转换是一个好主意。然而,这个转换函数应该返回什么呢?这个函数也许会返回一个年月日的值,也可以返回从 1970年1月1日起走过的秒数。这两种方式都可以有效表示,当问题是没有单一的一对一的映射,所以最好是不要定义转换操作符。相反,类应该定义一个或多个常规的成员函数来提取这些信息。
 

类型转换操作符可能会产生意外的结果

在实践中,类极少提供转换操作符。用户经常会惊讶于一个转换自动就发生了,而不是显式进行转换。然而,有一个重要的例外是:定义从类类型到 bool 值的转换并不少见。
在标准的早期版本中,类想要定义到 bool 的转换会面临一个问题:由于 bool 是算术运算符,类类型对象如果被转为 bool ,那么它可以用于任何算术类型被期待的上下文中。这种转换可能会发生在令人吃惊的地方。特别是,比如 istream 有一个转为 bool 的转换符,下面的代码将是合法的:
 

显式转换操作符

为了阻止上面的问题,新标准中引入了 显式的转换操作符(explicit conversion operators):
与显式的构造函数一样,编译器不会自动运用显式的转换操作符进行隐式转换:
如果转换操作符是显式的,依然可以做转换。然而,除了一个例外之外,我们必须使用cast进行显式转换。
这个例外是编译器会将 explicit 转换用在条件中,如下:
  • 如果条件是一个if, while 或者do语句
  • 如果条件表达式在for语句的头部
  • 如果作为逻辑非(!)、或(|)或者与(&&)操作符的操作数
  • 条件操作符(?:)的条件表达式部分中

转换为 bool 值

在早期版本的标准中,IO 类型定义了 void* 类型的转换。这样就避免了上面的问题。在新标准下,IO 库定义了explicit转换为bool的方法。
最佳实践 将类转为bool通常是用于条件,因而,operator bool 通常应该被定义为explicit 的。
 
 

避免转换二义性

如果一个类有一个或多个转换,重要是确保从类类型到目标类型只有一条路径。如果有超过一条路径来执行转换,那么想要写出无二义性的代码将会很难。
有两种方式会导致多中转换路径。第一种是两个类都提供了相互的转化。如,相互转换存在于当类 A 定义了转换构造函数其接收类 B 的对象作为参数,并且 B 自身定义了一个转换操作符用于转为类型 A。
第二中方式是定义了多个从或到相互之间可以转换的类型的转换。最明显的例子就是内置算术类型。一个给定的类应该最多定义一个从或到算术类型的转型。
警告:通常,给类之间定义相互转换,或者定义从或到两个以上的算术类型的转换是个坏主意。
 

参数匹配和相互转换

下面的例子将定义两种方式用于从A到B的转换:通过B的转换操作符或者通过A的构造函数其参数是B:
由于有两种方式从 B 得到一个 A,编译其不知道该运行哪个转换;调用f便是模糊的。这个调用可以使用A的构造函数并接收B作为参数,或者可以使用B的转换操作符将B转为A。由于两个函数是一样好的,这个调用便是错误的。
 
如果想要使用这个调用,就不得不显式调用转换操作符或者构造函数:
注意:不能用强制类型转换来解决二义性,这是由于强制类型转换本身就有相同的二义性问题。
 

二义性和多重转换到内置类型

另外如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性的问题。最简单的例子就是类当中定义了多个参数都是算术类型的构造函数,或者转换目标都是算术类型的类型转换运算符。
如下面的类型定义两个从不类型的算术类型的转换构造函数,已经两个到不同类型的算术类型的转换操作符:
f2 的调用中,两个转化都不是精确匹配long double类型。然而,两个转化都可以使用,并在之后跟者一个标准转换从而得到一个long double。因此,没有一个转换是由于另外一个的;这个调用便是模糊的。
当视图用long初始化a2时遇到了通常的问题。没有一个构造函数是精确匹配long的。每个都需要参数在被构造函数前进行转换,要么是从longdouble的转换,要么是从longint的转换。这两个转换序列都是可行的,因而调用是模糊的。
调用f2以及初始化a2是模糊的,这是由使用到的标准转换具有相同的优先级。当用户定义的转换被使用时,标准转换的优先级将被用于选择最佳的匹配,如:
注:当两个用户定义的转换被使用时,标准转换的优先级如果有的话(在转换函数之前或之后运用),将被用于选择最佳匹配。
 

重载函数和转换构造函数

在多个转换中选择一个在我们调用重载函数时将更为复杂。如果两个或多个转换提供可行匹配,那么这些转换被认为是一样好。例如,当调用重载函数其参数是不同的类类型,但是定义了相同的转换构造函数时会发生二义性错误:
CD都有构造函数接收int类型的参数。任何一个都可以用于匹配manip的一个版本。因此,调用是模糊的。可以通过显式构建正确的类型对象来消除二义性。如:manip(C(10));
在调用重载函数的过程中需要使用构造函数或者强制类型转换来转换参数通常意味着差的设计。
 
警告:转换和操作符
正确设计重载操作符,转换构造函数以及转换函数需要小心。特别是,如果类同时定义了转换操作符和重载操作符时容易产生二义性。下面这些规则将会有所帮助:
  • 不要定义相互转换类,如果类 Foo 有一个构造函数以类 Bar 的对象为参数,不要在 Bar 中定义转换操作符到类型 Foo;
  • 避免转换内置算术类型,特别是如果你已经定义了一个到算术类型的转换,那么:
    • 不要定义以算术类型为参数的重载操作符。如果用户需要使用这些操作符,转换操作会将你的类型的对象到内置类型,然后使用内置类型的操作符进行计算;
    • 不要定义转换到超过一个算术类型。让标准转换提供到其它算术类型的转换;
最简单的规则是:除了定义 explicit 转换到 bool ,避免定义转换函数,并且限制非 explicit 构造函数。
 

重载函数和用户定义的转换

如果重载函数的一个调用,有两个或更多的用户定义的转换提供可行匹配,那么转换被认为是一样好的。任何标准转换的优先级都不会被考虑。内置转换是否需要只有重载集合只有在调用匹配时使用相同的转换函数(the same conversion function),意思是只有在都使用相同的用户定义的转换之后才会考虑接下来的标准转换。
以下,manip 即便在其中一个类的构造函数的参数需要标准转换也是二义性调用:
在这个例子中,C 有一个从 int 而来的转换,E 有一个从 double 而来的转换。调用 manip2(10) ,两个 manip2 函数都是可行的:
  • manip2(const C&) 是可行的,原因是 C 有一个接收 int 的转换构造函数,这个构造函数的参数是精确匹配的;
  • manip2(const E&) 是可行的,原因是 E 有一个接收 double 的转换构造函数,我们可以使用标准转换来转换int;
由于重载函数需要不同的用户定义的转换,这个调用就是模糊的。在这里即便其中一个需要标准转换,另外一个是精确匹配,编译器依然认为这个调用是错误的。
注意 在重载函数的调用中,只有在可行函数(viable functions)需要相同的用户定义转换时,其优先级才会被考虑。如果需要不同的用户定义转换,那么这个调用就是模糊的。
 

函数匹配和重载操作符

重载的操作符是重载的函数。普通的函数匹配用于决定使用哪个操作符(内置的还是重载)运用于给定的表达式。然而,当一个操作符函数被用于表达式中,候选的函数集合要多于常规的函数调用,如果 a 是一个类类型,表达式 a sym b 可能是:
不同于常规的函数调用,不能使用调用的形式来区分我们是使用成员函数还是非成员函数。
当将一个重载的操作符用于类类型的操作数时,候选的函数包括常规的非成员版本的操作符,以及内置版本的操作符。如果左边的操作数是类类型,并且如果它定义了成员版本的重载操作符也会被包含进来。
 
当调用一个命名函数时,相同名字的成员和非成员函数并不会彼此重载。这是由于调用成员函数和非成员函数的语法形式是决然不同的。当一个调用是通过类类型对象(或者引用或者指针)时,那么只有那个类的成员函数会被考虑。当将重载操作符用于表达式中时,没有任何东西来指示我们是使用成员还是非成员函数。因此,成员和非成员版本都必须被考虑。
注意 在表达式中使用的操作符的候选函数集合同时包括成员和非成员函数。
 
如:
可以把两个SmallInt对象做加法,但是当想进行混合加法时会陷入二义性问题:
第二个加法之所以是模糊的,原因在于我们可以将 0 转为 SmallInt 然后使用 SmallInt 的 operator+ 做运算,或者将 s3 转为 int 然后使用内置加法;
警告 为同一个类同时提供转换到算术类型的转换函数和重载操作符可能会在重载操作符和内置操作符之间导致二义性。
  • C++
  • 重载 new 和 delete继承
    目录