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