被vector狠狠地刺痛了
揭示STL重要更改
预备知识:
l 理解标准C++ 0x 的concepts,例如:auto 关键字,lambda 表达式、右值引用等。
l 熟练使用STL。熟悉2个及以上STL容器的使用。
l 你手上必须有有VC2010的编译器,或者其它支持最新的C++标准和更新Stl的编译器。
这篇文章介绍了新版STL修订内容。这些变化是TR1中最为关注的内容(译注1);以下是STL的新增特性:
l Constant迭代器
l array类
l tuple类
l
l 随机生成器类(
l 对sets及无序sets容器的改进
l 对maps及无序maps的改进
l 正则表达式
l 功能的改进及实用的头文件
l 加强指针管理类
Constant 迭代器
首先说明,constant迭代器并不等于const迭代器。constant迭代器是const_iterator。 将const关键字加在 iterator前时,其所指的变量是无法修改的。而const_iterator则在首次赋值后不再可指向其它变量(类似:常量指针和指向常量的指针)看了下面代码后,你应该会有一个清晰的认识:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
using
namespace
std;
vector<
int
> IntVector;
...
// Normal iterator
vector<
int
>::iterator iter_nc = IntVector.begin();
*iter_nc = 100;
// Valid
iter_nc = IntVector.end();
// Valid
// Constant Iterator
vector<
int
>::const_iterator iter_c = IntVector.begin();
*iter_c = 100;
// INVALID!
iter_c = IntVector.end();
// Valid
// The 'const' iterator
const
vector<
int
>::iterator iter_const = IntVector.begin();
*iter_const = 100;
// Valid
iter_const = IntVector.end();
// Invalid (Why? Learn C++!)
// The 'const' Constant Iterator
const
vector<
int
>::const_iterator iter_const_c = IntVector.begin();
*iter_const_c = 100;
// Invalid!
iter_const_c = IntVector.end();
// Invalid
|
新特性
容器中新增函数可以明确返回Constant迭代器。在此之前,我们先了解一下迭代器返回规则:
1. 如果容器是constant,或者类方法前申明const,则将返回const_iterator。
2. 如果左值是const_iterator,普通的iterator将被返回,但会向下转型为const_iterator;
3. 其它情况,将返回普通iterator;
注:大多数迭代器访问方法(eg:begin,end)有相应的重载版本以返回constant 或non-const迭代器 ;
因此,你可明确指明返回的是constant还是普通迭代器;
以下为新增方法:
访问方法 |
含义 |
cbegin |
返回容器第一个元素的const_iterator . |
cend |
返回容器最后一个元素后一位的 const_iterator . |
crbegin |
返回rbegin 的 const_iterator 。即最后一个元素的constant迭代器. |
crend |
返回rend的 const_iterator 。即第一个元素前一位的constant迭代器. |
以上所有方法都在相应的容器类中申明为const。
为什么要引入这些方法?
你当然可以指定你需要那种迭代器,可能你根本就不需要这些新的方法;
加入这些方法的主要原因是因为新修订的auto关键词的引入;如果你已经了解auto关键词的含义,你应该知道我们可以在变量申明时不用明确指定类型。编译器会根据表达式的右值推导变量类型;当有如下调用:
1
|
auto
iter = IntVector.begin();
|
可以指定是普通还是constant迭代器被返回。因此,为了返回一个constant迭代器(const_iterator),你可以使用:
1
2
|
auto
iter = IntVector.cbegin();
// cbegin
// The return type is: vector
|
这样,这个迭代器以只读模式遍历vector,可以写出如下代码:
1
|
for
(
auto
iter = IntVector.cbegin(); iter != IntVector.cend(); ++iter) { }
|
需要说明的是,constant迭代器可以访问 非const成员方法(我不敢保证,我只是在源码中看到)。因此,最好使用cend来代替end检查有效性。
在sets和maps集合中又是如何?
对于所有的集合类(set,multiset, unordered_set, and unordered_multiset),其迭代器总是constant的。也就是说,当调用方法begin/end,find 以及下标操作符[],都是返回的constant 迭代器。像begin,find这些方法,虽然它们返回的数据类型都是非constant迭代器,但其行为是constant迭代器。
看以下代码:
1
2
3
4
5
6
|
set<
int
> is;
is.insert(10);
*is.begin() = 120;
wcout << is.count(10) <<
", "
<< is.count(120);
|
以上例子首先插入10到set中,然后试图修改第一个元素的值。
因为只有一个元素插入进来,begin将返回指向该元素的迭代器;在vc9及之前的编译器中,可成功修改值为120;
但从vc10开始,第三行将不能编译通过,编译器报错为:
error C3892: 'std::**::begin' : you cannot assign to a variable that is const
对于map,键不可修改,值可修改。以下是代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
|
map<
int
,
int
> imap;
imap.insert( make_pair( 1, 1028 ));
imap.insert( make_pair( 2, 2048 ));
imap.find(1)->second = 1024;
// Compiles
imap.find(2)->first = 4;
// Error
imap[2] = 2000;
*imap.begin()->first = 10;
// Error
*imap.begin()->second= 10;
// Compiles
*imap.cbegin()->second= 10;
// Error on VC10, since iterator is constant. NA for VC9
|
数组类array
array类是STL新增容器类,用于在存储一个固定大小的数组。数组大小与数据元素类型一同由模版参数指出。其指定的大小数值必须是一个常量运行时的(与其它C/c++数组一样)。不像其它容器能够增加或减少容器大小,array支持其它标准方法-如迭代,随机访问,交换数据,赋值等。
头文件:
命名空间:tr1.但由于'using tr1::array' 已被头文件包含。所有,在使用中,申明std就可以了。
示例:
array
第一个参数指定模版数据类型,第二个是一个常量编译时的整型。定义之后,IntArray的大小不能再改变。当然,除了int,可以使用其它的基本数据类型。如果要使用自定义类,则要实现复制构造函数,重载赋值操作符,比较操作符。同时,默认构造函数必须为公有。
这个类的引入是为了和其它STL容器进行无缝整合。例如,你使用vector或list,需要调用begin/end方法来访问元素。当你想将其作为普通数组使用时,代码会编译失败。这必须改变。用array类,你可以同时获得STL的灵活性和普通数据的性能。
依据最近的编译器报告(tr1 增刊),可以使用以下方式来初始化array:
array
如果忽略1个或多个元素没赋值,它们将被置为0.
如果对array没有做任何初始化操作,其所有元素处于未初始化状态。
array类支持以下STL标准方法:
l at, operator [] - 返回指定位置元素引用.
l back, front - 各自返回第一及最后一个元素位置引用;
l begin, cbegin, rbegin, crbegin -返回第一个元素迭代器;
l end, cend, rend, crend - 返回最后一个元素后一位的迭代器;
l empty - 判断容器是否为空。仅当数组大小为0时返回true;
l size, max_size - 返回array对象大小,该大小在编译期间确定;
以下方法需要重点说明:
array::assign 和 array:fill
这2个方法功能相同,实现将array所有元素赋值为一个给定值。此方法也可用来通过指定值替换array对象的所有元素。例如:
1
2
3
4
5
|
array<
int
,10> MyArray;
MyArray.fill(40);
// or 'assign'
for_each(MyArray.cbegin(), MyArray.cend(),
[](
int
n) { std::wcout << n <<
"\t"
;} );
|
将10个元素赋值为40,之后输出到控制台。
array::data
此方法返回数组第一个元素地址。与普通数组相比(eg:int_array[N]),此方法类似于表达式&int_array[0] 或者int_array。 此方法是由指针直接操作。const及非const方法都可用。例如:
1
2
3
|
int
* pArray = MyArray.data();
// Or
auto
pArray = MyArray.data();
|
array::swap(array&) 和swap(array&, array&)
交换两个数组大小和类型相同的array对象。第一个方法是非静态方法,实现将本数组内容与参数中的指定数组交换。第二个方法,携带2个array&参数,互换内容。例如:
1
2
3
4
5
6
7
8
9
|
typedef
array<
int
,10> MyArrayType;
MyArrayType Array1 = {1,2,4,8,16};
MyArrayType Array2;
Array2.assign(64);
Array1.swap(Array2);
// Or - swap(Array1, Array2);
// Array1 - 64, 64...64
// Array2 - 1,2,4,....0
|
如果试图交换不同类型数组,编译器会报错:
1
2
3
4
5
|
array<
int
,5> IntArrayOf5 = {1,2,3,4,5};
array<
int
,4> IntArrayOf4 = {10,20,40};
array<
float
,4> FloatArrayOf4;
IntArrayOf5.swap(IntArrayOf4);
// ERROR!
swap(IntArrayOf4, FloatArrayOf4);
// ERROR!
|
六种比较操作符
作为全局函数的==, !=, <, >, <=, 及>=可用来比较2个相同类型的数组对象:
1
2
3
4
5
6
7
8
|
typedef
array<
int
,10> MyArrayType;
MyArrayType Array1 = {1,2,4,8,16};
MyArrayType Array2;
Array2.assign(64);
if
(Array2 == Array1)
wcout <<
"Same"
;
else
wcout <<
"Not Same"
;
|
结果输出"Not Same" .
元组类tuple
STL程序员都知道pair 结构,它用来包装2个任意类型的元素。除了maps,在其它地方这个结构也非常有用,我们可以用来包装2个元素,而不用定义一个结构体。我们可通过first、 second变量取得这2个元素的值。
1
2
3
|
pair NameAndAge.first =
"Intel"
;
NameAndAge.second = 40;
|
程序员经常typedef 它们,这样变量就可以容易的申明并可将pair 传给函数。
但是,如果你需要保证多于2个的元素,该怎么做呢?
通常,你会定义一个结构体,或使用多重pair。这样的格式并不能与STL无缝整合。
tuple 类就是为此而生。这个类允许将2至10个元素包装在一起。
所有元素类型都可不同,如果要加入自定义,其依赖的操作必须事先定义。tuple类通过模版重载具体如何工作,已超出我的理解,这里我只说明它可以完成什么,以及我们可以在哪里使用。
l 头文件:
l 命名空间: tr1. 'using tr1::tuple' 已包含在 std .
申明2个元素元组:
tuple
在构造函数中初始化:
tuple
如果初始化,则必须初始化所有元素,以下方式报错:
tuple
这个错误可能不那么明显,但是你应该明白这个原则:必须初始化所有成员。
构造之后的初始化并不简单。在pair中,有make_pair 来辅助pair的初始化。自然的,tuple也有相应的辅助函数make_tuple。如果你已经读过或将要读VC2010并行编程,你会发现C++库中类似的完成类似的功能有其它新的make_函数。(译注2:并行编程地址)
下面例子说明如何初始化tuple对象:
1
|
TwoElement = make_tuple(400, 800);
|
跟tuple模版类一样,make_tuple 有传入2至10个参数的重载函数版本。make_tuple充分利用C++0x中右值引用的思想,完美的支持STL新特性。它允许复用同一对象而不是调用复制构造函数和赋值操作,以此提升整体性能。
我们可以结合make_tuple和auto关键词来灵活的定义一个tuple:
1
2
|
auto
Elements = make_tuple(20,
'X'
, 3.14159);
// Eq: tuple
|
如何访问tuple中的元素?
对于pair对象,我们都是简单的使用其内部定义的变量first/second来访问元素。但是tuple类中并没有定义这样的变量,我们需要通过辅助函数get来访问这些未命名变量。例如:
1
2
|
tuple int
nAge = get<1>(NameAgeGender);
|
get
get函数的索引必须是一个常量编译时的整型并在有效范围内。返回类型的推导也是常量时;以下代码编译器会报错:
1
2
|
int
nAge = get<0>(NameAgeGender);
// ERROR - cannot convert from 'string' to 'int'
|
这样也有助与发现潜在的代码缺陷:
1
2
|
char
cGender = get<1>(NameAgeGender);
// Leve 4 C4244 warning - 'int' to 'char'
// Should be get<2>
|
更重要的是,get可以赋值给auto关键词:
1
|
auto
sName = get<0>(NameAgeGender);
// Type deduction - 'string'
|
对于非const类型tuple对象,返回类型为元组元素的引用,这样,可以修改元素值:
1
2
|
// Modify Age
get<1>(NameAgeGender) = 79;
|
也可以将这个返回类型放在另一个引用中,并在之后修改。使用auto关键词也可行:
1
2
|
auto
& rnAge = get<1>(NameAgeGender) ;
rnAge = 79
|
需要指出的是,get函数同样适用于array类:
1
2
3
4
|
array<
char
, 5> Vowels = {
'A'
,
'E'
,
'I'
,
'o'
,
'U'
};
char
c = get<1>(Vowels);
// 'E'
get<3>(Vowels) =
'O'
;
// Modify
get<10> |
tie函数
这个函数实现make_tuple函数的逆操作。使用这个函数,你可以用一个tuple对象来初始化一组变量。看例子:
1
2
3
4
5
|
tuple string sName;
int
nAge;
char
cSex;
tie(sName, nAge, cSex) = NameAgeGender;
|
以上代码将这3个变量对应的设置为值 "Gandhi", 52, and 'M'。我想tie是否可以用于取代make_tuple?结果证明不行:
1
2
3
4
5
|
tuple NameAgeGender = tie(
"Gates"
, 46,
'M'
);
// make_tuple
string sName;
int
nAge;
char
cSex;
make_tuple(sName, nAge, cSex) = NameAgeGender;
// NO error. See below.
|
tie函数返回tuple对象的引用;make_tuple则是创建一个对象,返回的并非引用。上面最后一行试图修改一个临时创建的tuple对象,编译器不报错,却并不能得到正确结果。因此,最好还是按照它们设计的功能来使用这2个函数。
六种关系操作符
与array类一样,tuple类中同样实现了6种操作符的功能。所有的重载版本都要求作关系操作的两个tuple对象必须是同类型的(所包含的元素的对应位置的类型必须相同)。
(未完待续)
【注1】TR1:
C++ Technical Report 1 (TR1)是ISO/IEC TR 19768, C++ Library Extensions(函式库扩充)的一般名称。TR1是一份文件,内容提出了对C++标准函式库的追加项目。这些追加项目包括了正则表达式、智能指针、哈希表、随机数生成器等。TR1自己并非标准,他是一份草稿文件。然而他所提出的项目很有可能成为下次的官方标准。这份文件的目标在于「为扩充的C++标准函式库建立更为广泛的现成实作品」。
原文:http://www.codeproject.com/KB/stl/stl2010.aspx