深入理解C++11(三)(摘录)

深入理解C++11(三)

列表初始化
为了统一初始化方式,并且让初始化行为具有确定的效果,C++11中提出了列表初始化(List-initialization)的概念。
1.3.1 统一的初始化
在上面我们已经看到了,对于普通数组和POD类型,C++98/03可以使用初始化列表(initializer list)进行初始化:

int i_arr[3] = { 1, 2, 3 };
long l_arr[] = { 1, 3, 2, 4 };
struct A
{
int x;
int y;
} a = { 1, 2 };

但是这种初始化方式的适用性非常狭窄,只有上面提到的这两种数据类
型可以使用初始化列表。

在C++11中,初始化列表的适用性被大大增加了。它现在可以用于任何类型对象的初始化,如代码清单1-8所示。
代码清单1-8 通过初始化列表初始化对象:

class Foo
{ public:
Foo(int) {}
private:
Foo(const Foo &);
};
int main(void)
{
Foo a1(123);
Foo a2 = 123; // error: 'Foo::Foo(const Foo &)' is private
Foo a3 = { 123 };
Foo a4 { 123 };
int a5 = { 3 };
int a6 { 3 };
return 0;
}

在上例中,a3、a4使用了新的初始化方式来初始化对象,效果如同a1的直
接初始化。
a5、a6则是基本数据类型的列表初始化方式。可以看到,它们的形式都是
统一的。
这里需要注意的是,a3虽然使用了等于号,但它仍然是列表初始化,因
此,私有的拷贝构造并不会影响到它。
a4和a6的写法,是C++98/03所不具备的。在C++11中,可以直接在变量名
后面跟上初始化列表,来进行对象的初始化。

这种变量名后面跟上初始化列表方法同样适用于普通数组和POD类型的初始化:

int i_arr[3] { 1, 2, 3 }; // 普通数组
struct A
{
int x;
struct B
{
int i;
int j;
} b;
} a { 1, { 2, 3 } }; // POD 类型

在初始化时,{}前面的等于号是否书写对初始化行为没有影响。

另外,如同读者所想的那样,new操作符等可以用圆括号进行初始化的地方,也可以使用初始化列表:

int* a = new int { 123 };
double b = double { 12.12 };
int* arr = new int[3] { 1, 2, 3 };

指针a指向了一个new操作符返回的内存,通过初始化列表方式在内存初
始化时指定了值为123。
b则是对匿名对象使用列表初始化后,再进行拷贝初始化。
这里让人眼前一亮的是arr的初始化方式。堆上动态分配的数组终于也可
以使用初始化列表进行初始化了。

除了上面所述的内容之外,列表初始化还可以直接使用在函数的返回值上:

struct Foo
{
Foo(int, double) {}
};
Foo func(void)
{
return { 123, 321.0 };
}

这里的return语句就如同返回了一个Foo(123321.0)。

由上面的这些例子可以看到,在C++11中使用初始化列表是非常便利的。它不仅统一了各种对象的初始化方式,而且还使代码的书写更加简单清晰。

1.3.2 列表初始化的使用细节
在C++11中,初始化列表的使用范围被大大增强了。一些模糊的概念也随之而来。
上一节,读者已经看到了初始化列表可以被用于一个自定义类型的初始化。但是对于一个自定义类型,初始化列表现在可能有两种执行结果:

struct A
{
int x;
int y;
} a = { 123, 321 }; // a.x = 123, a.y = 321
struct B
{
int x;
int y;
B(int, int) : x(0), y(0) {}
} b = { 123, 321 }; // b.x = 0, b.y = 0

其实,上述变量a的初始化过程是C++98/03中就有的聚合类型
(Aggregates)的初始化。它将以拷贝的形式,用初始化列表中的值来初始化
struct A中的成员。
struct B由于定义了一个自定义的构造函数,因此,实际上初始化是以构
造函数进行的。

看到这里,读者可能会希望能够有一个确定的判断方法,能够清晰地知道初始化列表的赋值方式。具体来说,在使用初始化列表时,对于什么样的类型C++会认为它是一个聚合体

下面来看看聚合类型的定义:

(1)类型是一个普通数组(如int[10]、char[]、long[2][3])。
(2)类型是一个类(class、struct、union),且
·无用户自定义的构造函数。
·无私有(Private)或保护(Protected)的非静态数据成员。
·无基类。
·无虚函数。
·不能有{}=直接初始化(brace-or-equal-initializer)的非静态数据成
员。

对于数组而言,情况是很清晰的。只要该类型是一个普通数组,哪怕数组的元素并非一个聚合类型,这个数组本身也是一个聚合类型:

int x[] = { 1, 3, 5 };
float y[4][3] =
{
{ 1, 3, 5 },
{ 2, 4, 6 },
{ 3, 5, 7 },
};
char cv[4] = { 'a', 's', 'd', 'f' };
std::string sa[3] = { "123", "321", "312" };

下面重点介绍当类型是一个类时的情况。首先是存在用户自定义构造函数时的例子,代码如下:

struct Foo
{
int x;
double y;
int z;
Foo(int, int) {}
};
Foo foo { 1, 2.5, 1 }; // error

这时无法将Foo看做一个聚合类型,因此,必须以自定义的构造函数来构
造对象。

私有(Private)或保护(Protected)的非静态数据成员的情况如下:

struct ST
{
int x;
double y;
protected:
int z;
};
ST s { 1, 2.5, 1 }; // error
struct Foo
{
int x;
double y;
protected:
static int z;
};
Foo foo { 1, 2.5 }; // ok

在上面的示例中,ST的初始化是非法的。因为ST的成员z是一个受保护的
非静态数据成员。
而Foo的初始化则是成功的,因为它的受保护成员是一个静态数据成员。
这里需要注意,Foo中的静态成员是不能通过实例foo的初始化列表进行初
始化的,它的初始化遵循静态成员的初始化方式。

对于有基类和虚函数的情况:

struct ST
{
int x;
double y;
virtual void F(){}
};
ST s { 1, 2.5 }; // error
struct Base {};
struct Foo : public Base
{
int x;
double y;
};
Foo foo { 1, 2.5 }; // error

ST和Foo的初始化都会编译失败。因为ST中存在一个虚函数F,而Foo则有一
个基类Base。

最后,介绍“不能有{}和=直接初始化(brace-or-equal-initializer)的非静态
数据成员”这条规则,代码如下:

struct ST
{
int x;
double y = 0.0;
};
ST s { 1, 2.5 }; // error

在ST中,y在声明时即被=直接初始化为0.0,因此,ST并不是一个聚合类
型,不能直接使用初始化列表。
在C++98/03中,对于y这种非静态数据成员,本身就不能在声明时进行这
种初始化工作。但是在C++11中放宽了这方面的限制。可以看到,在C++11
中,非静态数据成员也可以在声明的同时进行初始化工作(即使用{}=进行
初始化)。

对于一个类来说,如果它的非静态数据成员在声明的同时进行了初始化,那么它就不再是一个聚合类型,因此,也不能直接使用初始化列表。对于上述非聚合类型的情形,想要使用初始化列表的方法就是自定义一个构造函数,比如:

struct ST
{
int x;
double y;
virtual void F(){}
private:
int z;
public:
ST(int i, double j, int k) : x(i), y(j), z(k) {}
};
ST s { 1, 2.5, 2 };

需要注意的是,聚合类型的定义并非递归的。简单来说,当一个类的非静态成员是非聚合类型时,这个类也有可能是聚合类型。比如下面这个例子:

struct ST
{
int x;
double y;
private:
int z;
};
ST s { 1, 2.5, 1 }; // error
struct Foo
{
ST st;
int x;
double y;
};
Foo foo { {}, 1, 2.5 }; // OK

可以看到,ST并非一个聚合类型,因为它有一个Private的非静态成员。
但是尽管Foo含有这个非聚合类型的非静态成员st,它仍然是一个聚合类
型,可以直接使用初始化列表。
注意到foo的初始化过程,对非聚合类型成员st做初始化的时候,可以直
接写一对空的大括号“{}”,相当于调用ST的无参构造函数。

现在,对于使用初始化列表时的一些细节有了更深刻的了解。对于一个聚合类型,使用初始化列表相当于对其中的每个元素分别赋值;而对于非聚合类型,则需要先自定义一个合适的构造函数,此时使用初始化列表将调用它对应的构造函数
1.3.3 初始化列表
读者可能注意到了,C++11中的stl容器拥有和未显示指定长度的数组一样的初始化能力,代码如下:

int arr[] { 1, 2, 3 };
std::map<std::string, int> mm =
{
{ "1", 1 }, { "2", 2 }, { "3", 3 }
};
std::set<int> ss = { 1, 2, 3 };
std::vector<int> arr = { 1, 2, 3, 4, 5 };

这里arr没有显式指定长度,因此,它的初始化列表可以是任意长度。
同样,std::map、std::set、std::vector也可以在初始化时任意书写
需要初始化的内容。
前面自定义的Foo却不具备这种能力,只能按部就班地按照构造函数指定
的参数列表进行赋值。

实际上,stl中的容器是通过使用std::initializer_list这个轻量级的类模板来完成上述功能支持的。我们只需要为Foo添加一个std::initializer_list构造函数,它也将拥有这种任意长度初始化的能力,代码如下:

class Foo
{ public:
Foo(std::initializer_list<int>) {}
};
Foo foo = { 1, 2, 3, 4, 5 }; // OK!

那么,知道了使用std::initializer_list来接收{…},如何通过它来给自定义容器赋值呢?来看代码清单1-9中的例子:

class FooVector
{
std::vector<int> content_;
public:
FooVector(std::initializer_list<int> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
content_.push_back(*it);
}
}
};
class FooMap
{
std::map<int, int> content_;
using pair_t = std::map<int, int>::value_type;
public:
FooMap(std::initializer_list<pair_t> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
content_.insert(*it);
}
}
};
FooVector foo_1 = { 1, 2, 3, 4, 5 };
FooMap foo_2 = { { 1, 2 }, { 3, 4 }, { 5, 6 } };

这里定义了两个自定义容器,一个是FooVector,采用std::vector<int>
作为内部存储;另一个是FooMap,采用std::map<intint>作为内部存储。
可以看到,FooVector、FooMap的初始化过程,就和它们使用的内部存储
结构一样。
这两个自定义容器的构造函数中,std::initializer_list负责接收初始化列
表。并通过我们熟知的for循环过程,把列表中的每个元素取出来,并放入内
部的存储空间中

std::initializer_list不仅可以用来对自定义类型做初始化,还可以用来传递同类型的数据集合,代码如下:

void func(std::initializer_list<int> l)
{
for (auto it = l.begin(); it != l.end(); ++it)
{
std::cout << *it << std::endl;
}
}
int main(void)
{
func({}); // 一个空集合
func({ 1, 2, 3 }); // 传递{ 1, 2, 3 }
return 0;
}

如上述所示,在任何需要的时候,std::initializer_list都可以当作参数来
一次性传递同类型的多个数据。

std::initializer_list的一些细节
了解了std::initializer_list之后,再来看看它的一些特点,如下:

·它是一个轻量级的容器类型,内部定义了iterator等容器必需的概念。
·对于std::initializer_list而言,它可以接收任意长度的初始化列表,但
 要求元素必须是同种类型T(或可转换为T)。
·它有3个成员接口:size()、begin()、end()。
·它只能被整体初始化或赋值。

通过前面的例子,已经知道了std::initializer_list的前几个特点。其中没有涉及的接口size()是用来获得std::initializer_list的长度的,比如:

std::initializer_list<int> list = { 1, 2, 3 };
size_t n = list.size(); // n == 3

最后,对std::initializer_list的访问只能通过begin()和end()进行循环遍历,遍历时取得的迭代器是只读的。因此,无法修改std::initializer_list中某一个元素的值,但是可以通过初始化列表的赋值对std::initializer_list做整体修改,代码如下

std::initializer_list<int> list;
size_t n = list.size(); // n == 0
list = { 1, 2, 3, 4, 5 };
n = list.size(); // n == 5
list = { 3, 1, 2, 4 };
n = list.size(); // n == 4

std::initializer_list拥有一个无参数的构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list。之后,我们对std::initializer_list进行赋值操作(注意,它只能通过初始化列表赋值),可以发现std::initializer_list被改写成了{1,2,3,4,5}。然后,还可以对它再次赋值,std::initializer_list被修改成了{3,1,2,4}。

看到这里,可能有读者会关心std::initializer_list的传递或赋值效率。假如std::initializer_list在传递或赋值的时候如同vector之类的容器一样,把每个元素都复制了一遍,那么使用它传递类对象的时候就要斟酌一下了。实际上,std::initializer_list是非常高效的。它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了列表中元素的引用而已

因此,我们不应该像这样使用:

std::initializer_list<int> func(void)
{
int a = 1, b = 2;
return { a, b }; // a、b在返回时并没有被拷贝
}

虽然这能够正常通过编译,但却无法传递出我们希望的结果(a、b在函数
结束时,生存期也结束了,因此,返回的将是不确定的内容)。

这种情况下最好的做法应该是这样:

std::vector<int> func(void)
{
int a = 1, b = 2;
return { a, b };
}

使用真正的容器,或具有转移/拷贝语义的物件来替代std::initializer_list返回需要的结果。我们应当总是把std::initializer_list看做保存对象的引用,并在它持有对象的生存期结束之前完成传递
防止类型收窄
类型收窄指的是导致数据内容发生变化或者精度丢失的隐式类型转换。考虑下面这种情况:

struct Foo
{
Foo(int i) { std::cout << i << std::endl; }
};
Foo foo(1.2);

以上代码在C++中能够正常通过编译,但是传递之后的i却不能完整地保存一个浮点型的数据。

上面的示例让我们对类型收窄有了一个大概的了解。具体来说,类型收窄包括以下几种情况:

1)从一个浮点数隐式转换为一个整型数,如int i=2.2。
2)从高精度浮点数隐式转换为低精度浮点数,如从long double隐式转换
为double或float。
3)从一个整型数隐式转换为一个浮点数,并且超出了浮点数的表示范
围,如float x=(unsigned long long)-1。
4)从一个整型数隐式转换为一个长度较短的整型数,并且超出了长度较
短的整型数的表示范围,如char x=65536。

在C++98/03中,像上面所示类型收窄的情况,编译器并不会报错(或报一个警告,如Microsoft Visual C++)。这往往会导致一些隐藏的错误。在C++11中,可以通过列表初始化来检查及防止类型收窄:

int a = 1.1; // OK
int b = { 1.1 }; // error
float fa = 1e40; // OK
float fb = { 1e40 }; // error
float fc = (unsigned long long)-1; // OK
float fd = { (unsigned long long)-1 }; // error
float fe = (unsigned long long)1; // OK
float ff = { (unsigned long long)1 }; // OK
const int x = 1024, y = 1;
char c = x; // OK
char d = { x }; // error
char e = y; // OK
char f = { y }; // OK

在上面的各种隐式类型转换中,只要遇到了类型收窄的情况,初始化列
表就不会允许这种转换发生。

其中需要注意的是x、y被定义成了const int。如果去掉const限定符,那么最后一个变量f也会因为类型收窄而报错。

对于类型收窄的编译错误,不同的编译器表现并不相同。
在gcc 4.8中,会得到如下警告信息:

warning: narrowing conversion of '1.0e+40' from 'double' to
'float' inside { } [-Wnarrowing]
float fb = { 1e40 };

在Microsoft Visual C++2013中,同样的语句则直接给出了一个错误:

error C2397: conversion from 'double' to 'float' requires a
narrowing conversion

另外,对于精度不同的浮点数的隐式转换,如下面这种:

float ff = 1.2; // OK
float fd = { 1.2 }; // OK (gcc)

fd的初始化并没有引起类型收窄,因此,在gcc 4.8下没有任何错误或警
告。但在Microsoft Visual C++2013中,fd的初始化语句将会得到一个error
C2397的编译错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值