(本文是Nicolai M.Josuttis所著的The C++ Standard Library - A Tutorial and Reference的个人读书笔记)
2.1.2 C++98 和 C++11的兼容性
基本C++11编译器是兼容C++98或者C++03风格的代码的,但是由于引入了新关键字,自然旧风格的代码中不应该随意使用新的关键字当作变量名
如果需要将不同的代码按照不同C++版本进行编译,可以采用宏定义的方式来指定C++版本
#define __cplusplus 201103L
#define __cplusplus 199711L
(具体数值请查阅编译器手册)
兼容性只对源代码文件生效,与二进制文件无关。这将会导致一些问题:比如令现存的函数得到一个新的返回类型,因为不允许通过返回类型来区分重载函数。通常来说,使用C++11编译器来编译C++98程序是没有问题的,但是使用C++11编译器来链接C++98二进制文件是有可能出错的。
3.1.1 书写格式调整
模板中的空格:不再需要在”>“之间增加空格
vector<list<int> >; // 所有C++版
vector<list<int>>;//自C++11以后
nullptr和std::nullptr_t:使用nullptr来表示一个空指针(这个指针不指向任何值),这种做法可以避免空指针和int类型的0之间的二义性
nullptr是一个新的关键字,它会自动转换成对应的指针类型,但是不会转换成整型。它的类型是std::nullptrt,因此你甚至可以重载传递空指针的函数。std::nullptr_t被视为是一种基本类型
3.1.2 auto自动类型推导
在C++11中可以使用auto关键字来声明对象。使用auto关键字以后,将会根据初始化的类型来推导类型,所以使用auto时必须初始化。
当对象是一个非常复杂的类型的时候,auto关键字非常有用
vector<string> v;
auto pos = v.begin(); //vector<string>::iterator
auto l = [] (int x)->bool{...,}; //lambda类型,参数为int,返回bool
3.1.3 通用初始化和初始化列表
C++11引入了通用初始化(列表初始化)的概念来统一繁杂的初始化方法。使用{}可以进行通用初始化,并且适用于所有初始化。
int values[]{1,2,3};
std::vector<int> v{2,3,5,7,11,13,17};
std::vector<std::string> cities {
"Berlin","New York","London","Braunschweig", "Cairo","Cologne"
};
std::complex<double> c{4.0,3.0};
初始化列表会进行值初始化,就是基本类型的局部变量会被初始化为0
int i; //undefined
int j{};//0
int*p;//undefined
int*q{};//nullptr
使用初始化列表时,将不会执行类型转化。
为了支持用户定义类型使用初始化列表,C++11提供了类模板std::initializer_list<>,它可以支持初始化多个数值,或者是在其它地方使用多个数值
void print(std::initializer_list<int> vals)
{
for (auto p = vals.begin(); p!=val.end(); ++p){
std::cout << *p << "n";
}
}
print({12,3,5,7,11,13,17});
使用初始化列表类型来作为构造函数的参数
class P
{
public:
P(int,int);
P(std::initializer_list<int>);
};
P p(77,5); // 调用 P::P(int,int)
P q{77,5}; // 调用 P::P(initializer_list)
P r{77,5,42}; // 调用 P::P(initializer_list)
P s = {77,5}; // 调用 P::P(initializer_list)
由于初始化列表的出现,可以使用explicit参数来避免传入多个参数时自动类型转换。
class P
{
public:
P(int a, int b) {
...
}
explicit P(int a, int b, int c) {
...
}
};
P x(77,5); // OK
P y{77,5}; // OK
P z {77,5,42}; // OK
P v = {77,5}; // OK 允许隐式转换
P w = {77,5,42}; // ERROR 因为禁止隐式转换
3.1.4 for遍历指定区域
C++11引入了新类型的for,用来遍历给定的范围
for (decl: coll) {
statement
}
coll是集合,decl是coll中每个元素的声明
当遍历容器时,应当将decl中的元素声明为引用类型。否则,statement将会作用于一个容器元素的局部拷贝之上。为了避免调用拷贝构造函数和析构函数,声明元素应当使用常引用类型。
以下三种形式是等价的
for ( decl : coll ) {
statement
}
for (auto _pos=coll.begin(), _end=coll.end(); _pos!=_end; ++_pos ) {
decl = *_pos;
statement
}
for (auto _pos=begin(coll), _end=end(coll); _pos!=_end; ++_pos ) {
decl = *_pos;
statement
}
3.1.5 移动语义和右值引用
移动语义可以有效地避免不必要的复制和临时变量,以下是一个简单的介绍。
样例代码
void createAndInsert (std::set<X>& coll)
{
X x; // 创建一个X类型的对象
...
coll.insert(x); // 将这个对象作为参数传入insert函数中
}
当往集合中插入一个元素时,集合提供了创建实参的内部拷贝的成员函数
namespace std {
template <typename T, ...> class set {
public:
... insert (const T& v); // 常引用会导致拷贝
...
};
}
这种拷贝行为是有效的,因为集合提供了值语义。此外,集合能够插入临时对象、被使用的对象和插入后可修改的对象。
X x;
coll.insert(x); // 传入x的拷贝
...
coll.insert(x+x); // 传入一个临时的左值(x+x的结果)的拷贝
...
coll.insert(x); // 传入x的拷贝即使x之后不会被使用
但是,对于后两个插入而言,由于x+x和x之后不会在被使用,所以更好的方法是移动临时量的内容作为新的元素,尤其是拷贝操作非常耗时的时候。这种移动临时量的操作就是移动语义。C++11以后,程序员可以使用move来移动将来不会被使用的临时量。
X x;
coll.insert(x); // 传入x的拷贝
...
coll.insert(x+x); // 移动临时量
...
coll.insert(std::move(x)); // 移动(或者是拷贝)x
<utility>的std::move()函数的实现并非使用任何形式的移动,而仅仅是将参数转换为一个右值引用。右值引用的声明需要使用2个&符号。右值引用代表可以修改的右值(匿名临时变量,只能出现赋值语句的右边),右值不再被需要,因此它的内容和资源可以被”窃取“。
现在,集合可以提供一种insert的重载形式,它可以处理这些右值引用
namespace std {
template <typename T, ...> class set {
public:
... insert (const T& x); // 左值调用
... insert (T&& x); // 右值调用
...
};
}
右值引用将会”窃取“x的资源,这种行为需要知道x的类型,因为只有x的类型才能访问它的内部成员。在x类型中应当提供移动构造函数,来简化拷贝操作
class X {
public:
X (const X& lvalue); // copy constructor
X (X&& rvalue); // move constructor
...
};
除了”窃取者“以外,被移动的对象不应该被其它角色修改。通常需要将被传递的变量清空
类似地,非简单类也应该提供移动赋值运算符重载
class X {
public:
X& operator= (const X& lvalue); //拷贝构造函数
X& operator= (X&& rvalue); //移动构造函数
...
};
对于字符串和集合而言,这些操作可以通过简单地交换内部内容和资源来完成。但是,最好还是将*this的内容清空,因为对象也可能持有资源,比如锁。
最后,注意到移动语义两个最重要的特征:
(1)为左值引用和右值引用提供重载
(2)返回左值引用
左值引用和右值引用的重载规则
void foo(X&);
只能被左值调用
void foo(const X&);
左右值均可调用
void foo(X&);//同时写两种形式
void foo(X&&);//重载
左值和右值调用会调用不同的重载形式
void foo(const X&);
void foo(X&&);
和前面一样
void foo(X&&);//只定义了一种形式
foo()被左值调用时会触发编译错误,这种性质可以用来编写不希望被左值调用的函数
从上面可以看出,如果类没有提供移动语义,只拥有拷贝构造函数和拷贝赋值运算符重载,类将会调用左值引用。总体而言,std::move()意味着在定义了移动构造函数/移动赋值运算符重载的时候会调用移动语义,否则就调用拷贝语义。
返回值是不应该move的。根据语法规则,检查下面的代码:
X foo ()
{
X x;
...
return x;
}
如果X拥有可执行的拷贝构造函数和移动构造函数,编译器会忽略拷贝构造函数,只采用移动构造函数。这就是所谓的RVO(命名返回值优化)
如果X只拥有移动构造函数,x被移动
如果X只拥有拷贝构造函数,x被拷贝
如果X没有移动构造函数或拷贝构造函数,那么将不会通过编译
注意到,如果返回对象是一个局部非静态的对象,返回它的右值引用也是一个错误行为。
X&& foo ()
{
X x;
...
return x; // ERROR: 返回不存在的对象的引用
}
右值引用也是引用,当返回一个局部对象的时候,意味着返回一个对不存在对象的引用,无论是否使用std::move()都会有问题。
3.1.6 新的字符串字面值常量
在C++11以后,你可以定义原始字符串和宽字节字符串常量
原始字符串常量
原始字符串可以定义一个字符序列,序列的内容和打印值完全一致(避免转义)。只需要在字符串前面增加一个字符R,并使用()包括即可。
为了在原始字符串中支持")"符号,可以使用区域限定符。完整的格式是R"限定()限定",限定应该是最长16字节的字符序列(不包括破折号、空格和圆括号)
R"nc(a
bnc()"
)nc";
等价于
"an bnc()"n "
原始字符串常量在定义规则表达式时非常有用。
编码字符串常量
使用编码前缀,你可以为字符串字面值定义特殊的字符编码方式。有以下几种情况:
u8定义了UTF-8编码
u定义了char16_t字符序列
U定义了char32_t字符序列
L定义了wchar_t宽字符序列
L"hello"//定义了"hello" 为 wchar_t字符序列
在原始字符串常量的R之前使用编码前缀也可以定义它的字符编码方式。