From:http://blog.jobbole.com/55063/
本节内容:auto、decltype、基于范围的for语句、初始化列表、统一初始化语法和语义、右值引用和移动语义、Lambdas、noexcept防止抛出异常、constexpr、nullptr——一个指针空值常量、复制并再抛出异常、内联命名空间、用户自定义数据标识。
auto
推导
1
|
auto x = 7;
|
在这里因为它的初始化类型我们将得到x的int类型。一般来说,我们可以写
1
|
auto x = expression;
|
x的类型我们将会根据初始化表达式“ expression”的类型来自动推导。
当一个变量的类型很难准确的知道或者写出的时候,用atuo通过初始化表达式的类型进行推导显然是非常有用的。
参考:
1
2
3
4
5
|
template
<
class
T>
void
printall(
const
vector<T>& v)
{
for
(auto p = v.begin(); p!=v.end(); ++p)
cout << *p <<
"\n"
;
}
|
在C++98里我们必须这样写
1
2
3
4
5
|
template
<
class
T>
void
printall(
const
vector<T>& v)
{
for
(
typename
vector<T>::const_iterator p = v.begin(); p!=v.end(); ++p)
cout << *p <<
"\n"
;
}
|
当一个变量的类型取决于模板实参的时候不用auto真的很难定义,例如:
1
2
3
4
5
6
|
template
<
class
T,
class
U>
void
multiply(
const
vector<T>& vt,
const
vector<U>& vu)
{
// ...
auto tmp = vt[i]*vu[i];
// ...
}
|
从T*U的表达式,我们人类的思维是很难理清tmp的类型的,但是编译器肯定知道T和U经过了什么特殊处理。
auto的特性在最早提出和应用时是有区别的:1984年初,Stroustrup在他的Cfront实现里用到了它,但是是由于C的兼容问题被迫拿来用的,这些兼容的问题已经在C++98和C++99接受了隐性int时候消失了。也就是说,现在这两种语言要求每一个变量和函数都要有一个明确的类型定义。auto旧的含义(即这是一个局部变量)现在是违法的。标准委员会的成员们在数百万行代码中仅仅只找到几百个用到auto关键字的地方,并且大多数出现在测试代码中,有的甚至就是一个bug。
auto在代码中主要是作为简化工具,并不会影响标准库规范。
参考:
decltype
decltype(E)的类型(“声明类型”)可用名称或表达E来声明。例如:
1
2
3
4
5
6
7
8
9
|
void
f(
const
vector<
int
>& a, vector<
float
>& b)
{
typedef
decltype(a[0]*b[0]) Tmp;
for
(
int
i=0; i<b.size(); ++i) {
Tmp* p =
new
Tmp(a[i]*b[i]);
// ...
}
// ...
}
|
这个概念已经流行在泛型编程的标签“typeof”很长一段时间,但实际使用的“领域”的实现是不完整和不兼容,所以标准版命名了decltype。
注意:喜欢使用auto时,你只是需要一个变量的类型初始化。你真的需要decltype如果你需要一个类型的东西不是一个变量,例如返回类型。
参考:
基于范围的for循环
声明的范围像是STL-sequence定义的begin()和end(),允许你在这个范围内循环迭代。所有标准容器可以作为一个范围来使用,比如可以是std::string,初始化器列表,一个数组,和任何你可以定义begin()和end()的,比如istream。例如:
1
2
3
4
5
|
void
f(vector<
double
>& v)
{
for
(auto x : v) cout << x <<
'\n'
;
for
(auto& x : v) ++x;
// using a reference to allow us to change the value
}
|
你可以看到,V中所有的元素都从begin()开始迭代循环到了end()。另一个例子:
1
|
for
(
const
auto x : { 1,2,3,5,8,13,21,34 }) cout << x <<
'\n'
;
|
begin()(和end())可以被做为x.begin()的成员或一个独立的函数被称为开始(x)。成员版本优先。
参考:
初始化列表
推导
1
2
3
4
5
6
7
8
9
|
vector<
double
> v = { 1, 2, 3.456, 99.99 };
list<pair<string,string>> languages = {
{
"Nygaard"
,
"Simula"
}, {
"Richards"
,
"BCPL"
}, {
"Ritchie"
,
"C"
}
};
map<vector<string>,vector<
int
>> years = {
{ {
"Maurice"
,
"Vincent"
,
"Wilkes"
},{1913, 1945, 1951, 1967, 2000} },
{ {
"Martin"
,
"Ritchards"
}, {1982, 2003, 2007} },
{ {
"David"
,
"John"
,
"Wheeler"
}, {1927, 1947, 1951, 2004} }
};
|
初始化列表不再只针对于数组了。定义一个接受{}初始化列表的函数(通常是初始化函数)接受一个std::initializer_list < T >的参数类型,例如:
1
2
3
4
5
6
|
void
f(initializer_list<
int
>);
f({1,2});
f({23,345,4567,56789});
f({});
// the empty list
f{1,2};
// error: function call ( ) missing
years.insert({{
"Bjarne"
,
"Stroustrup"
},{1950, 1975, 1985}});
|
初始化器列表可以是任意长度的,但必须同种类型的(所有元素必须的模板参数类型,T,或可转换T)。
一个容器可能实现一个初始化列表构造函数如下:
1
2
3
4
5
6
7
8
9
10
|
template
<
class
E>
class
vector {
public
:
vector (std::initializer_list<E> s)
// initializer-list constructor
{
reserve(s.size());
// get the right amount of space
uninitialized_copy(s.begin(), s.end(), elem);
// initialize elements (in elem[0:s.size()))
sz = s.size();
// set vector size
}
// ... as before ...
};
|
直接初始化和复制初始化的区别是对初始化列表的维护,但是因为初始化列表的相关联的频率就降低了。例如std::vector有一个int类型显示构造函数和initializer_list构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
vector<
double
> v1(7);
// ok: v1 has 7 elements
v1 = 9;
// error: no conversion from int to vector
vector<
double
> v2 = 9;
// error: no conversion from int to vector
void
f(
const
vector<
double
>&);
f(9);
// error: no conversion from int to vector
vector<
double
> v1{7};
// ok: v1 has 1 element (with its value 7.0)
v1 = {9};
// ok v1 now has 1 element (with its value 9.0)
vector<
double
> v2 = {9};
// ok: v2 has 1 element (with its value 9.0)
f({9});
// ok: f is called with the list { 9 }
vector<vector<
double
>> vs = {
vector<
double
>(10),
// ok: explicit construction (10 elements)
vector<
double
>{10},
// ok explicit construction (1 element with the value 10.0)
10
// error: vector's constructor is explicit
};
|
函数可以作为一个不可变的序列访问initializer_list。例如:
1
2
3
4
|
void
f(initializer_list<
int
> args)
{
for
(auto p=args.begin(); p!=args.end(); ++p) cout << *p <<
"\n"
;
}
|
仅具有一个std::initializer_list的单参数构造函数被称为初始化列表构造函数。
标准库容器,string类型及正则表达式均具有初始化列表构造函数,以及(初始化列表)赋值函数等。一个初始化列表可被用作Range,例如,表达式Range。
初始化列表是一致泛化初始化解决方案的一部分。他们还防止类型收窄。一般来说,你应该通常更喜欢使用{ }来代替()初始化,除非你想用c++98编译器来分享代码或(很少)需要使用()调用没initializer_list重载构造函数。
参考:
统一初始化语法和语义
c++ 98提供了几种方法初始化一个对象根据其类型和初始环境。滥用时,会产生可以令人惊讶的错误和模糊的错误消息。推导:
1
2
3
4
|
string a[] = {
"foo"
,
" bar"
};
// ok: initialize array variable
vector<string> v = {
"foo"
,
" bar"
};
// error: initializer list for non-aggregate vector
void
f(string a[]);
f( {
"foo"
,
" bar"
} );
// syntax error: block as argument
|
和
1
2
3
4
|
int
a = 2;
// "assignment style"
int
aa[] = { 2, 3 };
// assignment style with list
complex z(1,2);
// "functional style" initialization
x = Ptr(y);
// "functional style" for conversion/cast/construction
|
和
1
2
3
|
int
a(1);
// variable definition
int
b();
// function declaration
int
b(foo);
// variable definition or function declaration
|
要记得初始化的规则并选择最好的方法去初始化是比较难的。
C++11的解决方法是允许所有的初始化使用初始化列表
1
2
3
4
5
6
7
8
9
10
11
|
X x1 = X{1,2};
X x2 = {1,2};
// the = is optional
X x3{1,2};
X* p =
new
X{1,2};
struct
D : X {
D(
int
x,
int
y) :X{x,y} {
/* ... */
};
};
struct
S {
int
a[3];
S(
int
x,
int
y,
int
z) :a{x,y,z} {
/* ... */
};
// solution to old problem
};
|
重点是,x{a}在在执行代码中都创建了一个相同的值,所以在使用“{}”进行初始化合法的情况下都产生了相同的结果。例如:
1
2
3
4
5
|
X x{a};
X* p =
new
X{a};
z = X{a};
// use as cast
f({a});
// function argument (of type X)
return
{a};
// function return value (function returning X)
|
参考:
右值引用和移动语义
左值(用在复制操作符左边)和右值(用在复制操作符右边)的区别可以追溯到Christopher Strachey (C++遥远的祖先语言CPL和外延语义之父)的时代。在C++中,非const引用可以绑定到左值,const引用既可以绑定到左值也可以绑定要右值。但是右值却不可以被非const绑定。这是为了防止人们改变那些被赋予新值之前就被销毁的临时变量。例如:
1
2
3
4
|
void
incr(
int
& a) { ++a; }
int
i = 0;
incr(i);
// i becomes 1
incr(0);
// error: 0 in not an lvalue
|
如果incr(0)被允许,那么就会产生一个无法被人看到的临时变量被执行增加操作,或者更糟的0会变成1.后者听起来很傻,但实际上确实存在这样一个bug在Fortran编译器中:为值为0的内存位置分配。
到目前为止还好,但考虑以下代码:
1
2
3
4
5
6
|
template
<
class
T> swap(T& a, T& b)
// "old style swap"
{
T tmp(a);
// now we have two copies of a
a = b;
// now we have two copies of b
b = tmp;
// now we have two copies of tmp (aka a)
}
|
如果T是一个复制元素要付出昂贵代价的类型,比如string和vector,swap将会变成一个十分昂贵的操作(对于标准库来说,我们有专门化的string和vector来处理)。注意一下这些奇怪的现象:我们并不想任何变量拷贝。我们仅仅是想移动变量a,b和tmp的值。
在C++11中,我们可以定义“移动构造函数”和“移动赋值操作符”来移动,而不是复制他们的参数:
1
2
3
4
5
6
7
8
|
template
<
class
T>
class
vector {
// ...
vector(
const
vector&);
// copy constructor
vector(vector&&);
// move constructor
vector& operator=(
const
vector&);
// copy assignment
vector& operator=(vector&&);
// move assignment
};
// note: move constructor and move assignment takes non-const &&
// they can, and usually do, write to their argument
|
& &表明“右值引用”。一个右值引用可以绑定到一个右值(而不是一个左值):
1
2
3
4
5
6
|
X a;
X f();
X& r1 = a;
// bind r1 to a (an lvalue)
X& r2 = f();
// error: f() is an rvalue; can't bind
X&& rr1 = f();
// fine: bind rr1 to temporary
X&& rr2 = a;
// error: bind a is an lvalue
|
赋值这个操作的背后思想,并不是拷贝,它只是构造一个源对象的代表然后再替换。例如,string s1 = s2的移动,它不是产生s2的拷贝,而是让s1把s2中字符变为自己的同时删除自己原有的字符串(也可以放在s2中,但是它也面临着被销毁)
我们如何知道是否可以简单的从源对象进行移动?我们可以告诉编译器:
1
2
3
4
5
6
7
|
template
<
class
T>
void
swap(T& a, T& b)
// "perfect swap" (almost)
{
T tmp = move(a);
// could invalidate a
a = move(b);
// could invalidate b
b = move(tmp);
// could invalidate tmp
}
|
move(x)只是意味着你“你可以把x当作一个右值”,
如果把move()称做eval()也许会更好,但是现在move()已经用了好多年了。在c++11中,move()模板(参考简介)和右值引用都可以使用。
右值引用也可以用来提供完美的转发。
在C++0x的标准库中,所有的容器都提供了移动构造函数和移动赋值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最终结果是,在无用户干预时,标准容器和算法的性能都提升了,因为复制操作的减少。
参考:
lambdas
Lambda表达式是一种描述函数对象的机制,它的主要应用是描述某些具有简单行为的函数(译注:Lambda表达式也可以称为匿名函数,具有复杂行为的函数可以采用命名函数对象,当然,简单和复杂之间的划分依赖于编程人员的选择)。例如:
1
2
3
4
5
6
|
vector<
int
> v = {50, -10, 20, -30};
std::sort(v.begin(), v.end());
// the default sort
// now v should be { -30, -10, 20, 50 }
// sort by absolute value:
std::sort(v.begin(), v.end(), [](
int
a,
int
b) {
return
abs
(a)<
abs
(b); });
// now v should be { -10, 20, -30, 50 }
|
参数 [&](int a, int b) { return abs(a) < abs(b); }是一个"lambda"(又称为"lambda函数"或者"lambda表达式"), 它描述了这样一个函数操作:接受两个整形参数a和b,然后返回对它们的绝对值进行"<"比较的结果。(译注:为了保持与代码的一致性,此处应当为"[] (int a, int b) { return abs(a) < abs(b); }",而且在这个lambda表达式内实际上未用到局部变量,所以 [&] 是无必要的)
一个Lambda表达式可以存取在它被调用的作用域内的局部变量。例如:
1
2
3
4
5
6
7
8
9
|
void
f(vector<Record>& v)
{
vector<
int
> indices(v.size());
int
count = 0;
generate(indices.begin(),indices.end(),[&count](){
return
count++; });
// sort indices in the order determined by the name field of the records:
std::sort(indices.begin(), indices.end(), [&](
int
a,
int
b) {
return
v[a].name<v[b].name; });
// ...
}
|
有人认为这“相当简洁”,也有人认为这是一种可能产生危险且晦涩的代码的方式。我的看法是,两者都正确。
[&] 是一个“捕捉列表(capture list)”,用于描述将要被lambda函数以引用传参方式使用的局部变量。如果我们仅想“捕捉"参数v,则可以写为: [&v]。而如果我们想以传值方式使用参数v,则可以写为:[=v]。如果什么都不捕捉,则为:[]。将所有的变量以引用传递方式使用时采用 [&], [=] 则相应地表示以传值方式使用所有变量(译注:“所有变量”即指lambda表达式在被调用处,所能见到的所有局部变量)。
如果某一函数的行为既不通用也不简单,那么我建议采用命名函数对象或者函数。例如,如上示例可重写为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void
f(vector<Record>& v)
{
vector<
int
> indices(v.size());
int
count = 0;
generate(indices.begin(),indices.end(),[&](){
return
++count; });
struct
Cmp_names {
const
vector<Record>& vr;
Cmp_names(
const
vector<Record>& r) :vr(r) { }
bool
operator()(
int
a,
int
b)
const
{
return
vr[a].name<vr[b].name; }
};
// sort indices in the order determined by the name field of the records:
std::sort(indices.begin(), indices.end(), Cmp_names(v));
// ...
}
|
对于简单的函数功能,比如记录名称域的比较,采用函数对象就略显冗长,尽管它与lambda表达式生成的代码是一致的。在C++98中,这样的函数对象在被用作模板参数时必须是非本地的(译注:即你不能在函数对象中像此处的lambda表达式那样使用被调用处的局部变量),然而在C++中(译注:意指C++0x),这不再是必须的。
为了描述一个lambda,你必须提供:
参考:
noexcept防止抛出异常
如果一个函数不能抛出异常或者一个程序没有对函数抛出的异常进行处理,那么这个函数可以用关键字noexcept进行修饰,例如:
1
2
3
4
5
6
7
|
extern
"C"
double
sqrt
(
double
) noexcept;
// will never throw
vector<
double
> my_computation(
const
vector<
double
>& v) noexcept
// I'm not prepared to handle memory exhaustion
{
vector<
double
> res(v.size());
// might throw
for
(
int
i; i<v.size(); ++i) res[i] =
sqrt
(v[i]);
return
res;
}
|
如果一个被noexcept修饰的函数抛出了异常(所以异常会跳出呗noexcept修饰的函数),程序会调用std::terminate()这个函数来终止程序。在对象被明确定义的状态下不能调用terminate();比如无法保证析构函数正常调用,不能保证栈的自动释放,也无法保证在遇到任何问题时重新启动。故意这样的使noexcept成为一种简单“粗暴”而有效的处理机制-它比旧的处理机制throw()动态抛出异常要有效的多。
它可以让一个函数根据条件来实现noexcept修饰。比如,一个算法可以根据他的模板参数来决定自己是否抛出异常。
1
2
3
4
5
6
|
template
<
class
T>
void
do_f(vector<T>& v) noexcept(noexcept(f(v.at(0))))
// can throw if f(v.at(0)) can
{
for
(
int
i; i<v.size(); ++i)
v.at(i) = f(v.at(i));
}
|
这里,第一个noexcept被用作操作符operator:如果if f(v.at(0))不能够抛出异常,noexcept(f(v.at(0)))则返回true,所以f()和at()是无法抛出异常noexcept。
noexcept()操作符是一个常量表达式,并且不计算表达式的值。
声明的通常形式是noexcept(expression),并且单独的一个“noexcept”关键字实际上就是的一个noexcept(true)的简化。一个函数的所有声明都必须与noexcept声明保持 兼容。
一个析构函数不应该抛出异常;通常,如果一个类的所有成员都拥有noexcept修饰的析构函数,那么这个类的析构函数就自动地隐式地noexcept声明,而与函数体内的代码没有关系。
通常,将某个抛出的异常进行移动操作是一个很坏的主意,所以,在任何可能的地方都用noexcept进行声明。如果某个类的所有成员都有使用noexcept声明的析构函数,那么这个类默认生成的复制或者移动操作(类的复制构造函数,移动构造函数等)都是隐式的noexcept声明。(?)
noexcept 被广泛地系统地应用在C++11的标准库中,以此来提供标准库的性能和满足标准库对于简洁性的需求。
参考:
constexpr
常量表达式机制:
考虑下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
|
enum
Flags { good=0, fail=1, bad=2, eof=4 };
constexpr
int
operator|(Flags f1, Flags f2) {
return
Flags(
int
(f1)|
int
(f2)); }
void
f(Flags x)
{
switch
(x) {
case
bad:
/* ... */
break
;
case
eof:
/* ... */
break
;
case
bad|eof:
/* ... */
break
;
default
:
/* ... */
break
;
}
}
|
在这里,常量表达式关键字constexpr表示这个重载的操作符“|”就应该像一个简单的表单一样,如果它的参数本身就是常量 ,那么这个操作符应该在编译时期就应该计算出它的结果来。
除了可以在编译时期被动地计算表达式的值之外,我们希望能够主动地要求表达式在编译时期计算其结果值,从而用作其它用途,比如对某个变量进行赋值。当我们在变量声明前加上constexpr关键字之后,可以实现这一功能,当然,它也同时会让这个变量成为常量。
1
2
3
4
5
6
|
constexpr
int
x1 = bad|eof;
// ok
void
f(Flags f3)
{
constexpr
int
x2 = bad|f3;
// error: can't evaluate at compile time
int
x3 = bad|f3;
// ok
}
|
通常,我们希望编译时期计算可以保护全局或者名字空间内的对象,对名字空间内的对象,我们希望它保存在只读空间内。
对于那些构造函数比较简单,可以成为常量表达式(也就是可以使用constexpr进行修饰)的对象可以做到这一点(?)
1
2
3
4
5
6
7
8
|
struct
Point {
int
x,y;
constexpr Point(
int
xx,
int
yy) : x(xx), y(yy) { }
};
constexpr Point origo(0,0);
constexpr
int
z = origo.x;
constexpr Point a[] = {Point(0,0), Point(1,1), Point(2,2) };
constexpr
int
x = a[1].x;
// x becomes 1
|
参考:
constexpr
specifiernullptr 一个指针空值常量
nullptr是一个指针空值常量,不是一个整数。
1
2
3
4
5
6
7
8
9
10
|
char
* p = nullptr;
int
* q = nullptr;
char
* p2 = 0;
// 0 still works and p==p2
void
f(
int
);
void
f(
char
*);
f(0);
// call f(int)
f(nullptr);
// call f(char*)
void
g(
int
);
g(nullptr);
// error: nullptr is not an int
int
i = nullptr;
// error: nullptr is not an int
|
参考:
复制并再抛出异常
你如何捕获一个异常然后把它抛出到另一个线程?使用标准文档18.8.5里描述的标准库的魔力方法吧。
exception_ptr current_exception(); 正在处理的异常(15.3)或者正在处理的异常的副本(拷贝)将返回一个exception_ptr 变量,如果当前没有遇到异常,返回值为一个空的exception_ptr变量。只要exception_ptr指向一个异常,那么至少在exception_ptr的生存期内,运行时能够保证被指向的异常是有效的。
void rethrow_exception(exception_ptr p);
template exception_ptr copy_exception(E e); 它的作用如同:
1
2
3
4
5
|
try
{
throw
e;
}
catch
(...) {
return
current_exception();
}
|
当我们需要将异常从一个线程传递到另外一个线程时,这个方法十分有用的。
内联命名空间
内联命名空间机制是通过一种支持版本更新的机制来支持库的演化,推导:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// file V99.h:
inline
namespace
V99 {
void
f(
int
);
// does something better than the V98 version
void
f(
double
);
// new feature
// ...
}
// file V98.h:
namespace
V98 {
void
f(
int
);
// does something
// ...
}
// file Mine.h:
namespace
Mine {
#include "V99.h"
#include "V98.h"
}
|
我们这里有一个命名空间Mine包含最新版本的(V99)和前一个版本(V98),如果你想要显式应用(某个版本的函数),你可以:
1
2
3
4
5
6
|
#include "Mine.h"
using
namespace
Mine;
// ...
V98::f(1);
// old version
V99::f(1);
// new version
f(1);
// default version
|
内联的关键是使内联命名空间的声明和直接在外围命名空间声明一样。
lnline是静态的及面向实现的设施,它由命名空间的设计者放置来帮助用户进行选择。对于Mine的用是不可以说“我想要的命名空间是V98而不是V99”。
参照:
用户自定义数据标识
C++提供了很多内置的数据标识符(2.14节变量)
built-in types (2.14 Literals):
1
2
3
4
5
6
7
|
123
// int
1.2
// double
1.2F
// float
'a'
// char
1ULL
// unsigned long long
0xD0
// hexadecimal unsigned
"as"
// string
|
然而,爱C++98里并没有用户自定义的数据标识符。这就有悖于甚至冲突“用户自定义类型和内置leiixng一样得到支持”的原则。特殊情况下,人们有这样的需求:
1
2
3
4
5
6
7
|
"Hi!"
s
// std::string, not ``zero-terminated array of char''
1.2i
// imaginary
123.4567891234df
// decimal floating point (IBM)
101010111000101b
// binary
123s
// seconds
123.56km
// not miles! (units)
1234567890123456789012345678901234567890x
// extended-precision
|
C++11支持“用户自定义数据标识”通过在变量名后面加一个后缀来标定所需类型,例如:
1
2
3
4
5
6
7
8
|
constexpr complex<
double
> operator
""
i(
long
double
d)
// imaginary literal
{
return
{0,d};
// complex is a literal type
}
std::string operator
""
s (
const
char
* p,
size_t
n)
// std::string literal
{
return
string(p,n);
// requires free store allocation
}
|
注意constexpr的使用可以在编译时期计算。有了这个功能,我们可以这样写: