购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.3 理解统一初始化

在C++11中,花括号初始化是数据初始化的统一方法。基于这个原因,它被称为 统一初始化 (uniform initialization)。它可以说是C++11中开发人员应该理解和使用的重要特性之一。它屏蔽了之前基本类型、聚合类型和非聚合类型,以及数组和标准容器初始化的差异。

1.3.1 准备工作

为了更好地理解本小节的内容,你需要掌握直接初始化(它根据一组显式的构造函数参数初始化对象)和复制初始化(它根据另一个对象初始化一个对象)方法。以下是两种初始化方法的简单示例:

记住这些,让我们探究一下如何执行统一初始化。

1.3.2 使用方式

要无差别地初始化对象,就用花括号初始化形式,即{},对于直接初始化和复制初始化,它都适用。当采用花括号初始化形式时,它们被称为直接列表初始化和复制列表初始化:

下面是一些统一初始化示例:

❍ 对于标准容器:

❍ 对于动态数组:

❍ 对于数组:

❍ 对于内置类型:

❍ 对于用户自定义类型:

❍ 对于用户自定义POD类型:

1.3.3 工作原理

在C++11之前,对象的初始化取决于它们的类型:

❍ 基本类型通过赋值进行初始化:

❍ 如果类中有转换构造函数,类对象也可以使用一个值通过赋值来初始化(在C++11之前,带有单个参数的构造函数称为 转换构造函数 ):

❍ 当提供参数时,非聚合类可以用圆括号(函数形式)初始化,并且只有在执行默认初始化时才不带圆括号(调用默认构造函数)。在下面这个例子中,foo是1.3.2节中定义的结构:

❍ 聚合类型和POD类型可以用花括号初始化形式初始化。在下面的例子中,bar是1.3.2节中定义的结构:

普通旧数据(Plain Old Data,POD)类型是一种简单的类型(具有由编译器提供或显式默认的特殊成员,并且占用连续的内存区域)且具有标准布局(类不包含语言特性,例如与C语言不兼容的虚函数,并且所有成员具有相同的访问控制方式)。POD类型的概念在C++20中已被弃用,取而代之的是简单标准布局的类型。

除了初始化数据的方法不同之外,还有一些限制。例如,初始化标准容器(除了复制构造函数)的唯一方法是先声明一个对象,然后将元素插入其中,但是std::vector是个例外,因为它可以通过数组赋值,而数组可以通过前面提到的聚合类初始化方法初始化。但是,动态分配的聚合类对象不能直接进行初始化。

1.3.2节中的所有例子都用的是直接初始化方法,但是复制初始化也可以采用花括号初始化形式。直接初始化和复制初始化在某些情况下是等价的,但是复制初始化不太灵活,因为它在其隐式转换过程中不考虑explicit构造函数,必须从初始化列表中产生对象,而直接初始化期望从初始化列表到构造函数的参数的隐式转换。因此,动态分配数组只能通过直接初始化方法进行初始化。

在前面示例展示的类定义中,foo是一个既有默认构造函数又有带参数的构造函数的类。用默认构造函数进行默认初始化,我们需要用空的花括号,即{}。用带参数的构造函数,我们必须在{}中提供相应参数的值。不像非聚合类类型,它们的默认初始化就是调用默认构造函数,对于聚合类型,默认初始化意味着用0值初始化。

可以初始化标准容器(例如vector和map),因为所有标准容器在C++11中都有一个额外的构造函数,它接受类型为std::initializer_list<T>的参数。这基本上是对T const类型元素数组的轻量级代理。这些构造函数根据初始化列表中的值初始化内部数据。

使用std::initializer_list进行初始化的方式如下:

❍ 编译器解析初始化列表中元素的类型(所有元素必须具有相同的类型)。

❍ 编译器使用初始化列表中的元素创建一个数组。

❍ 编译器创建一个std::initializer_list<T>对象来包装之前创建的数组。

❍ std::initializer_list<T>被作为参数传递给构造函数。

当使用花括号初始化时,初始化列表方式总是优先于其他构造函数。如果类中含有这样的构造函数,则当用到花括号初始化时会调用它:

这个优先级原则适用于所有的函数,而不仅仅是构造函数。在下面这个例子中,存在着两个签名相同的重载函数。使用初始化列表调用函数会解析为调用std::initializer_list重载版本:

然而,这样也可能会导致出现bug。以std::vector类型为例,在vector的构造函数中,有一个单参数(表示初始分配的元素个数)的构造函数,还有一个接受std::initializer_list作为参数的构造函数。如果我们的目的是创建一个有预分配个数元素的vector,那么花括号初始化将无效,因为使用花括号初始化时,以std::initializer_list为参数的构造函数会优先被调用:

上述代码不会创建一个有5个元素的vector,而是创建一个只有一个值为5的元素的vector。为了能够真正创建有5个元素的vector,必须使用圆括号的初始化方式:

另外需要注意的是,花括号初始化不允许收缩转换(narrowing conversion)。根据C++标准(参考标准的8.5.4节),收缩转换是隐式转换:

从浮点类型到整型的转换。

从long double到double或float,或者从double到float,除非源是一个常量表达式,并且转换后的实际值在可以表示的值范围内(即使丢失精度)。

从整型或无作用域枚举到浮点类型,除非源是常量表达式,并且转换后的实际值适配目标类型且再转换为原始类型时可以得到原始的值。

从整型或无作用域枚举到更小范围的整型(不能完全表示原始类型的所有值),除非源是常量表达式,并且转换后的实际值适配目标类型且再转换为原始类型时可以得到原始的值。

以下声明会导致编译失败,因为它们需要进行收缩转换:

为了修复这个问题,需要进行显式转换:

花括号初始化列表不是表达式,也没有类型。因此,decltype不能用在花括号初始化列表中,而且模板类型推导方法不能推导出与花括号初始化列表匹配的类型。

我们来看另外一个例子:

上述声明是正确的,因为有一个从int到float的隐式转换。表达式47/13的值是3,它被赋给float类型的变量f2。

1.3.4 更多

以下示例展示了直接列表初始化和复制列表初始化。在C++11中,所有这些表达式的推导类型均为std::initializer_list<int>:

C++17修改了列表初始化的规则,区分了直接列表初始化和复制列表初始化。类型推导的新规则如下:

❍ 对于复制列表初始化,如果列表中的所有元素具有相同的类型,auto会被推导为std::initializer_list<T>,否则推导格式错误。

❍ 对于直接列表初始化,如果列表中只有一个元素,auto会被推导为T,如果有不止一个元素,则推导格式错误。

基于这些新规则,之前的示例将发生如下变化(注释中描述了推导出的类型):

在这个示例中,a和c被推导为std::initializer_list<int>,b被推导为int,d用的是直接列表初始化而且初始化列表不止一个元素,所以导致编译失败。

1.3.5 延伸阅读

❍ 阅读1.1节,以了解C++中自动类型推导的工作原理。

❍ 阅读1.4节,以了解如何很好地执行类成员初始化。 4Q/09f5kthGzo0tEoyjKMET2CxhZ+JgjPvlJTByVGB0KEkjnrzMBipPWrPZaK7qd

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开