在C++11之前,具有单个形参的构造函数被视为转换构造函数(因为它接受另一种类型的值并由此创建该类型的新实例)。在C++11中,没有explicit说明符的构造函数都被视为转换构造函数,这样的构造函数定义了从参数的类型到类的类型的隐式转换。类还可以定义转换操作符,将类的类型转换为另一种指定的类型。所有这些在某些情况下都是有用的,但偶尔也会产生问题。在本节中,我们将学习如何使用explicit构造函数和转换操作符。
对于这一节内容,你需要熟悉转换构造函数和转换操作符。在本节中,你将学习如何编写explicit构造函数和转换操作符,以避免与类型进行隐式转换。使用explicit构造函数和转换操作符(称为用户自定义转换函数)使编译器在某些情况下能够产生编译错误,这样开发人员可以快速发现这些错误并修复它们。
要声明explicit构造函数和explicit转换操作符(无论它们是函数还是函数模板),请在声明中使用explicit说明符。
explicit构造函数和explicit转换操作符的例子如下:
要理解explicit构造函数的必要性以及它们是如何工作的,我们首先来看一下转换构造函数。下面的foo类有三个构造函数(除了输出一条消息,它们什么都不做):一个默认构造函数(不带形参)、一个接受int形参的构造函数,以及一个接受int和double形参的构造函数。从C++11开始,这些都被视为转换构造函数。该类还有一个转换操作符,它可以将foo类型的值转换为bool类型:
基于此,以下对象的定义是可能的(注意:下方注释代表控制台的输出):
变量f1和f2调用默认构造函数,f3、f4、f5和f6调用接受int形参的构造函数。注意,这些对象的所有定义都是等价的,即使它们看起来不同(f3使用函数形式进行初始化,f4和f6是用复制进行初始化,f5直接使用花括号初始化列表进行初始化)。类似地,f7、f8和f9调用两个形参的构造函数。
在本例中,变量f5和f6会输出foo(1),而f8和f9则会导致编译错误,因为初始化列表的元素必须都是整型的。
需要注意的是,如果foo定义了参数为std::initializer_list的构造函数,那么所有使用{}的地方都被解析为调用这个函数:
现在这些看起来基本都是对的,但是隐式构造函数支持的隐式转换可能不是我们想要的。首先,让我们看几个正确的示例:
把foo类类型转换为bool类型的转换操作符在需要使用布尔值的时候直接使用foo对象。示例如下:
前两个例子把foo对象用作布尔值是符合我们预期的。但是,后两个例子,一个是加法,一个是测试是否相等,它们也许是不对的,因为我们其实大概率是希望把两个foo对象相加以及测试两个foo对象是否相等,而不是针对它们隐式转换为的布尔值。
也许一个更实际的示例更能够帮助我们理解隐式转换可能发生的问题,这个示例是string_buffer实现。这个类的内部可能会包含一个字符缓冲区。
这个类提供了几个转换构造函数:一个默认构造函数、一个接受size_t形参,(该形参表示要预分配的缓冲区的大小)的构造函数,以及一个接受char指针(该指针用于分配和初始化内部缓冲区)的构造函数。简而言之,本例中使用的字符串缓冲区(string_buffer)的实现如下:
根据这个定义,我们可以构造以下对象:
b1对象用默认构造函数进行初始化,它的缓冲区是空的;b2用带一个表示大小的形参的构造函数进行初始化,参数表示其内部缓冲区的大小;b3用一个已经存在的缓冲区进行初始化,这个缓冲区的大小用于定义其内部缓冲区的大小,然后其值被复制至缓冲区。然而,相同的定义还支持以下对象定义:
在本例中,b4用一个字符('a')进行初始化。因为存在size_t的隐式转换,单参数的构造函数将会被调用。这里意图不清晰,也许应该用字符串"a"来代替字符'a',这样会调用第三个构造函数。
但是,b5的定义大概率会出错,因为MaxSize是一个枚举(代表ItemSizes),它应该与缓冲区的大小无关,编译器不会以任何方式标记类似这样的错误。我们应该首选作用域枚举,而不是无作用域枚举,因为无作用域枚举会把枚举隐式转换为int类型,而作用域枚举则不会出现这种情况。如果ItemSizes是作用域枚举,那么上述那种情况就不会出现。
当在构造函数的声明中使用explicit说明符时,该构造函数将变成explicit构造函数,并且不再允许class类型对象的隐式构造。为了举例说明这一点,我们将稍微更改一下string_buffer类,将所有构造函数声明为explicit:
这里的改动很小,但是前面示例中的b4和b5的定义不再有效,而且是不正确的。这是因为在重载解析期间需要指明应该调用什么构造函数,从char或int到size_t的隐式转换不再可用,结果导致b4和b5的编译错误。注意,b1、b2和b3的定义仍然是有效的定义,即使构造函数是explicit的。
在这种情况下,解决这个问题的唯一方法是提供从char或int到string_buffer的显式强制转换:
使用explicit构造函数,编译器能够立即标记错误情况,开发人员也可以相应地做出反应,要么用正确的值修复初始化,要么提供显式强制转换。
只有在使用复制初始化完成初始化时才会出现这种情况,而在使用函数初始化或通用初始化时则不会出现这种情况。
使用explicit构造函数,以下定义仍然可能是错误的:
与构造函数类似,转换操作符可以声明为explicit的(如前所述)。在这种情况下,从对象类型到转换操作符指定的类型的隐式转换不再可能实现,需要进行显式强制转换。考虑到b1和b2,它们是我们前面定义的string_buffer对象,通过显式转换操作符定义operator bool将不再可能执行以下操作:
相反,它们需要显式转换为bool类型:
两个bool值相加没有太大意义,上面的示例只是为了说明如何进行显式强制转换。当没有显式静态转换时,编译器会报错,这个时候你就会意识到可能是表达式本身有问题,或者说其实你有别的意图。
❍ 阅读1.3节,以了解花括号初始化是如何工作的。