C++11 中值得关注的几大变化(详解)
源文章来自前C++标准委员会的 Danny Kalev 的 The Biggest Changes in C++11 (and Why You Should Care),赖勇浩做了一个中文翻译在这里。所以,我就不翻译了,我在这里仅对文中提到的这些变化“追问为什么要引入这些变化”的一个探讨,只有知道为了什么,用在什么地方,我们才能真正学到这个知识。而以此你可以更深入地了解这些变化。所以,本文不是翻译。因为写得有些仓促,所以难免有问题,还请大家指正。
目录
Lambda 表达式
Lambda表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫“闭包”,如果 lambda 函数没有传回值(例如 void ),其回返类型可被完全忽略。 定义在与 lambda 函数相同作用域的变量参考也可以被使用。这种的变量集合一般被称作 closure(闭包)。我在这里就不再讲这个事了。表达式的简单语法如下,
[capture](parameters)->return_type {body}
原文的作者给出了下面的例子:
int main() { char s[]="Hello World!"; int Uppercase = 0; //modified by the lambda for_each(s, s+sizeof(s), [&Uppercase] (char c) { if (isupper(c)) Uppercase++; }); cout << Uppercase << " uppercase letters in: " << s <<endl; }
在传统的STL中for_each() 这个玩意最后那个参数需要一个“函数对象”,所谓函数对象,其实是一个class,这个class重载了operator(),于是这个对象可以像函数的式样的使用。实现一个函数对象并不容易,需要使用template,比如下面这个例子就是函数对象的简单例子(实际的实现远比这个复杂):
template <class T> class less { public: bool operator()(const T&l, const T&r)const { return l < r; } };
所以,C++引入Lambda的最主要原因就是1)可以定义匿名函数,2)编译器会把其转成函数对象。相信你会和我一样,会疑问为什么以前STL中的ptr_fun()这个函数对象不能用?(ptr_fun()就是把一个自然函数转成函数对象的)。原因是,ptr_fun() 的局限是其接收的自然函数只能有1或2个参数。
那么,除了方便外,为什么一定要使用Lambda呢?它比传统的函数或是函数对象有什么好处呢?我个人所理解的是,这种函数之年以叫“闭包”,就是因为其限制了别人的访问,更私有。也可以认为他是一次性的方法。Lambda表达式应该是简洁的,极私有的,为了更易的代码和更方便的编程。
自动类型推导 auto
在这一节中,原文主要介绍了两个关键字 auto 和 deltype,示例如下:
auto x=0; //x has type int because 0 is int auto c='a'; //char auto d=0.5; //double auto national_debt=14400000000000LL;//long long
auto 最大的好处就是让代码简洁,尤其是那些模板类的声明,比如:STL中的容器的迭代子类型。
vector<int>::const_iterator ci = vi.begin();
可以变成:
auto ci = vi.begin();
模板这个特性让C++的代码变得很难读,不信你可以看看STL的源码,那是一个乱啊。使用auto必需一个初始化值,编译器可以通过这个初始化值推导出类型。因为auto是来简化模板类引入的代码难读的问题,如上面的示例,iteration这种类型就最适合用auto的,但是,我们不应该把其滥用。
比如下面的代码的可读性就降低了。因为,我不知道ProcessData返回什么?int? bool? 还是对象?或是别的什么?这让你后面的程序不知道怎么做。
auto obj = ProcessData(someVariables);
但是下面的程序就没有问题,因为pObject的型别在后面的new中有了。
auto pObject = new SomeType<OtherType>::SomeOtherType();
自动化推导 decltype
关于 decltype
是一个操作符,其可以评估括号内表达式的类型,其规则如下:
- 如果表达式e是一个变量,那么就是这个变量的类型。
- 如果表达式e是一个函数,那么就是这个函数返回值的类型。
- 如果不符合1和2,如果e是左值,类型为T,那么decltype(e)是T&;如果是右值,则是T。
原文给出的示例如下,我们可以看到,这个让的确我们的定义变量省了很多事。
const vector<int> vi; typedef decltype (vi.begin()) CIT; CIT another_const_iterator;
还有一个适合的用法是用来typedef函数指针,也会省很多事。比如:
decltype(&myfunc) pfunc = 0; typedef decltype(&A::func1) type;
auto 和 decltype 的差别和关系
Wikipedia 上是这么说的(关于decltype的规则见上)
#include <vector> int main() { const std::vector<int> v(1); auto a = v[0]; // a 的类型是 int decltype(v[0]) b = 1; // b 的类型是 const int&, 因为函数的返回类型是 // std::vector<int>::operator[](size_type) const auto c = 0; // c 的类型是 int auto d = c; // d 的类型是 int decltype(c) e; // e 的类型是 int, 因为 c 的类型是int decltype((c)) f = c; // f 的类型是 int&, 因为 (c) 是左值 decltype(0) g; // g 的类型是 int, 因为 0 是右值 }
如果auto 和 decltype 在一起使用会是什么样子?能看下面的示例,下面这个示例也是引入decltype的一个原因——让C++有能力写一个 “ forwarding function 模板”,
template< typename LHS, typename RHS> auto AddingFunc(const LHS &lhs, const RHS &rhs) -> decltype(lhs+rhs) {return lhs + rhs;}
这个函数模板看起来相当费解,其用到了auto 和 decltype 来扩展了已有的模板技术的不足。怎么个不足呢?在上例中,我不知道AddingFunc会接收什么样类型的对象,这两个对象的 + 操作符返回的类型也不知道,老的模板函数无法定义AddingFunc返回值和这两个对象相加后的返回值匹配,所以,你可以使用上述的这种定义。
统一的初始化语法
C/C++的初始化的方法比较,C++ 11 用大括号统一了这些初始化的方法。
比如:POD的类型。
int arr[4]={0,1,2,3}; struct tm today={0};
关于POD相说两句,所谓POD就是Plain Old Data,当class/struct是极简的(trivial)、属于标准布局(standard-layout),以及他的所有非静态(non-static)成员都是POD时,会被视为POD。如:
struct A { int m; }; // POD struct B { ~B(); int m; }; // non-POD, compiler generated default ctor struct C { C() : m() {}; ~C(); int m; }; // non-POD, default-initialising m
POD的初始化有点怪,比如上例,new A; 和new A(); 是不一样的,对于其内部的m,前者没有被初始化,后者被初始化了(不同 的编译器行为不一样,VC++和GCC不一样)。而非POD的初始化,则都会被初始化。
从这点可以看出,C/C++的初始化问题很奇怪,所以,在C++ 2011版中就做了统一。原文作者给出了如下的示例:
C c {0,0}; //C++11 only. 相当于: C c(0,0); int* a = new int[3] { 1, 2, 0 }; /C++11 only class X { int a[4]; public: X() : a{1,2,3,4} {} //C++11, member array initializer };
容器的初始化:
// C++11 container initializer vector<string> vs={ "first", "second", "third"}; map singers = { {"Lady Gaga", "+1 (212) 555-7890"}, {"Beyonce Knowles", "+1 (212) 555-0987"}};
还支持像Java一样的成员初始化:
class C { int a=7; //C++11 only public: C(); };
Delete 和 Default 函数
我们知道C++的编译器在你没有定义某些成员函数的时候会给你的类自动生成这些函数,比如,构造函数,拷贝构造,析构函数,赋值函数。有些时候,我们不想要这些函数,比如,构造函数,因为我们想做实现单例模式。传统的做法是将其声明成private类型。
在新的C++中引入了两个指示符,delete意为告诉编译器不自动产生这个函数,default告诉编译器产生一个默认的。原文给出了下面两个例子:
struct A { A()=default; //C++11 virtual ~A()=default; //C++11 };
再如delete
struct NoCopy { NoCopy & operator =( const NoCopy & ) = delete; NoCopy ( const NoCopy & ) = delete; }; NoCopy a; NoCopy b(a); //compilation error, copy ctor is deleted
这里,我想说一下,为什么我们需要default?我什么都不写不就是default吗?不全然是,比如构造函数,因为只要你定义了一个构造函数,编译器就不会给你生成一个默认的了。所以,为了要让默认的和自定义的共存,才引入这个参数,如下例所示:
struct SomeType { SomeType() = default; // 使用编译器生成的默认构造函数 SomeType(OtherType value); };
关于delete还有两个有用的地方是
1)让你的对象只能生成在栈内存上:
struct NonNewable { void *operator new(std::size_t) = delete; };
2)阻止函数的其形参的类型调用:(若尝试以 double 的形参调用 f()
,将会引发编译期错误, 编译器不会自动将 double 形参转型为 int 再调用f()
,如果传入的参数是double,则会出现编译错误)
void f(int i); void f(double) = delete;
nullptr
C/C++的NULL宏是个被有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。但是在C++的时代,这就会引发很多问题。你可以上网看看。这是为什么需要 nullptr
的原因。 nullptr
是强类型的。
void f(int); //#1 void f(char *);//#2 //C++03 f(0); //二义性 //C++11 f(nullptr) //无二义性,调用f(char*)
所以在新版中请以 nullptr
初始化指针。
委托构造
在以前的C++中,构造函数之间不能互相调用,所以,我们在写这些相似的构造函数里,我们会把相同的代码放到一个私有的成员函数中。
class SomeType { private: int number; string name; SomeType( int i, string& s ) : number(i), name(s){} public: SomeType( ) : SomeType( 0, "invalid" ){} SomeType( int i ) : SomeType( i, "guest" ){} SomeType( string& s ) : SomeType( 1, s ){ PostInit(); } };
但是,为了方便并不足让“委托构造”这个事出现,最主要的问题是,基类的构造不能直接成为派生类的构造,就算是基类的构造函数够了,派生类还要自己写自己的构造函数:
class BaseClass { public: BaseClass(int iValue); }; class DerivedClass : public BaseClass { public: using BaseClass::BaseClass; };
上例中,派生类手动继承基类的构造函数, 编译器可以使用基类的构造函数完成派生类的构造。 而将基类的构造函数带入派生类的动作 无法选择性地部分带入, 所以,要不就是继承基类全部的构造函数,要不就是一个都不继承(不手动带入)。 此外,若牵涉到多重继承,从多个基类继承而来的构造函数不可以有相同的函数签名(signature)。 而派生类的新加入的构造函数也不可以和继承而来的基类构造函数有相同的函数签名,因为这相当于重复声明。(所谓函数签名就是函数的参数类型和顺序不)
右值引用和move语义
在老版的C++中,临时性变量(称为右值”R-values”,位于赋值操作符之右)经常用作交换两个变量。比如下面的示例中的tmp变量。示例中的那个函数需要传递两个string的引用,但是在交换的过程中产生了对象的构造,内存的分配还有对象的拷贝构造等等动作,成本比较高。
void naiveswap(string &a, string &b) { string temp = a; a=b; b=temp; }
C++ 11增加一个新的引用(reference)类型称作右值引用(R-value reference),标记为typename &&。他们能够以non-const值的方式传入,允许对象去改动他们。这项修正允许特定对象创造出move语义。
举例而言,上面那个例子中,string类中保存了一个动态内存分存的char*指针,如果一个string对象发生拷贝构造(如:函数返回),string类里的char*内存只能通过创建一个新的临时对象,并把函数内的对象的内存copy到这个新的对象中,然后销毁临时对象及其内存。这是原来C++性能上重点被批评的事。
能过右值引用,string的构造函数需要改成“move构造函数”,如下所示。这样一来,使得对某个stirng的右值引用可以单纯地从右值复制其内部C-style的指针到新的string,然后留下空的右值。这个操作不需要内存数组的复制,而且空的暂时对象的析构也不会释放内存。其更有效率。
class string { string (string&&); //move constructor string&& operator=(string&&); //move assignment operator };
The C++11 STL中广泛地使用了右值引用和move语议。因此,很多算法和容器的性能都被优化了。
C++ 11 STL 标准库
C++ STL库在2003年经历了很大的整容手术 Library Technical Report 1 (TR1)。 TR1 中出现了很多新的容器类 (unordered_set
, unordered_map
, unordered_multiset
, 和 unordered_multimap
) 以及一些新的库支持诸如:正则表达式, tuples,函数对象包装,等等。 C++11 批准了 TR1 成为正式的C++标准,还有一些TR1 后新加的一些库,从而成为了新的C++ 11 STL标准库。这个库主要包含下面的功能:
线程库
这们就不多说了,以前的STL饱受线程安全的批评。现在好 了。C++ 11 支持线程类了。这将涉及两个部分:第一、设计一个可以使多个线程在一个进程中共存的内存模型;第二、为线程之间的交互提供支持。第二部分将由程序库提供支持。大家可以看看promises and futures,其用于对象的同步。 async() 函数模板用于发起并发任务,而 thread_local 为线程内的数据指定存储类型。更多的东西,可以查看 Anthony Williams的 Simpler Multithreading in C++0x.
新型智能指针
C++98 的知能指针是 auto_ptr, 在C++ 11中被废弃了。
C++11 引入了两个指针类: shared_ptr 和 unique_ptr。 shared_ptr只是单纯的引用计数指针,unique_ptr 是用来取代
。 auto_ptr
unique_ptr
提供 auto_ptr
大部份特性,唯一的例外是 auto_ptr
的不安全、隐性的左值搬移。不像 auto_ptr
,unique_ptr
可以存放在 C++0x 提出的那些能察觉搬移动作的容器之中。
为什么要这么干?大家可以看看《More Effective C++》中对 auto_ptr的讨论。
新的算法
定义了一些新的算法: all_of()
, any_of()
和 none_of()。
#include <algorithm> //C++11 code //are all of the elements positive? all_of(first, first+n, ispositive()); //false //is there at least one positive element? any_of(first, first+n, ispositive());//true // are none of the elements positive? none_of(first, first+n, ispositive()); //false
使用新的copy_n()算法,你可以很方便地拷贝数组。
#include <algorithm> int source[5]={0,12,34,50,80}; int target[5]; //copy 5 elements from source to target copy_n(source,5,target);
使用 iota()
可以用来创建递增的数列。如下例所示:
include <numeric> int a[5]={0}; char c[3]={0}; iota(a, a+5, 10); //changes a to {10,11,12,13,14} iota(c, c+3, 'a'); //{'a','b','c'}
总之,看下来,C++11 还是很学院派,很多实用的东西还是没有,比如: XML,sockets,reflection,当然还有垃圾回收。看来要等到C++ 20了。呵呵。不过C++ 11在性能上还是很快。参看 Google’s benchmark tests。原文还引用Stroustrup 的观点:C++11 是一门新的语言——一个更好的 C++。
如果把所有的改变都列出来,你会发现真多啊。我估计C++ Primer那本书的厚度要增加至少30%以上。C++的门槛会不会越来越高了呢?我不知道,但我个人觉得这门语言的确是变得越来越令人望而却步了。(想起了某人和我说的一句话——学技术真的是太累了,还是搞方法论好混些?)
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《C++11 中值得关注的几大变化(详解)》的相关评论
辛苦了,学习学习!
弱弱的问一句,反射到底有什么好处?
C++感觉好大好复杂。
学了一部分,又被吓会C了 – –
收藏下先~~
Lambda 表达式还有一点好处,编译器可以把表达式内容内联到代码中,这样for_each和手写for循环将产生同样的代码
python不是有lambda吗。、,C++凑这热闹干嘛?尾大不掉
真有点望而却步了……要学的太多了!
lambda表达式和闭包是两个东西,匿名函数不一定是闭包,闭包也不需要是匿名函数
相当复杂啊……果然搞C++是个无止尽的活。
功能越来越强大,但强大的代价是复杂,复杂的结果是没几个人能正确使用。
举例来说:在不少人根本不知道左值右值的情况下,右值引用除了给一些语法专家们增加谈资以外,又有什么用呢?
@resty
对,闭包是可以持久引用的一个匿名函数的局部变量,实际上。
@局部变量
右值引用这种对库的编写者来说很有意义,因为其可以使得库的行为更符合预期(最小惊讶原则)
这里是比较详细的介绍:http://www2.research.att.com/~bs/C++0xFAQ.html
C++11虽然语法变得复杂,但很多语法并不高深。他让我们写代码变得更加快速、方便了
还是学C#好了
在wikipedia中对closure的介绍是“Such a function is said to be “closed over” its free variables.”,SICP中的介绍是“A closure is an implementation technique for representing procedures with free variables. ”,闭包一词应该是指函数中自由变量被绑定(封闭)了吧。
@Tiger Soldier
正解~
C++中的Lambda同样可以用做闭包。
其实这些东西在lisp里面早就有了。。。
而且lisp有的,其他语言还没有,比如很强大的宏机制。
@resty 呵呵,我没说它没有意义,只是说它太复杂,而且这个功能既然对外提供,相信使用它的就不止是精通c++(有几个人符合这个标准呢)的库作者,而一般人使用这些功能绝对会符合“最大惊讶原则”。
关于lambda,closure和object的关系,这篇文章做了介绍:http://www.cnblogs.com/weidagang2046/archive/2010/11/01/1865899.html
我的观点是在函数式编程中:object = lambda + closure
那个初始化的语法看着好别扭啊….
闭包的伟大之处在于,闭包访问的变量是lexical environment的而不是被调用处的上下文中的,从而编写需要传递一段code到某处执行的代码变的更容易,这段code看起来就像在它所在的位置被执行一样,写起来更直观以及和其他部分的代码更统一。
闭包的意义实质上c++ lamda的2进制模型说的很清楚了: 闭包是一个函数对象,“包”住了一个或多个上下文中的非全局非静态变量(up value, but not global value),至于包的方式是组合或者聚合,是否使用生命周期自动管理,和语言与实现有关,c++ 只区分包引用和包拷贝;另外还要“闭”,也就是封装好这些up value,不允许绕开函数调用就能够访问它们。
@陈皓
我知道C++的Lambda可以用作闭包。只是你的表述有很多问题不是吗?
比如:
Lambda表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫“闭包”
将Lambda这一段对lambda和closure的表述有各种前后矛盾的地方啊……
谢谢指正,我已修改了一下原文。
右值引用的理解肯定有难度,不过可以逼着很多人实践pimpl模式。终于可以甩开讨厌的swap语法了。
语法划分的越来越细,表明语义的研究的不断深入和语义的表达可以变得更精确。不过这并不影响一般人使用语言,对吧。有谁会因为汉语不断产生的新词汇和网络语言句式,从而“最大惊讶”了呢?理解语法,比读浩如烟海的api文档,轻松多了吧。
比如: XML,sockets,reflection,当然还有垃圾回收..
XML有w3c,
sockets有SUSv3/v4
reflection有可能
垃圾回收,c++有很多gc,但是很不可能成为标准
gc是被推迟讨论了,并且给了一些建议。
reflection绝对不能和字符串有关系,否则标准委员会的人只可能给你一个臭脸。
为什么我觉得c++更友好了呢?之前没有基础设施的年代,你要自己在coding中绕过种种陷阱,想cool,想快,带着boost src源代码在各platform间跑来跑去,跟各种同事解释,boost很好,领导嫌大,还要自己裁剪一份header only的来用,往事不堪回首啊,有标准了,至少可以名正言顺的用smart point了,可以不用跟各种老人家解释return一个对象编译器真的会优化了。好时代明显就要来了,为啥大家反而怕了呢?
这样就可以在模版函数里推导了,不需要less()了。
struct less
{
template
bool operator()(const T&l, const T&r)const
{
return l < r;
}
}
尖括号被吞了。
这样就可以在模版函数里推导了,不需要less<>()了。
struct less
{
template<>
bool operator()(const T&l, const T&r)const
{
return l < r;
}
}
@zy498420
点解
没有讲variadic template。
c++ 推出的这些新特性 是为了更加方便的写程序。
如果有些人觉得这些太复杂完全可以不管, 继续写你之前写的.
用过c++的人会觉得这些特性很自然早就该这么做了, 了解下语法.没什么学习曲线.
还有些说其他语言有这xx了c++就不必做这个, 这什么逻辑.
我想提案一个看起来不重要,但是其实蛮重要的东西。
定义一个基类(可以用模板特化手法生成),除了定义一个公有的虚析构函数以外什么都没有。
这样大家再也不用提醒去把某些基类的析构函数申明为虚的了,直接继承(多继承也无妨,放2奶位就是了。由于没有数据成员,菱形继承也不用怕)就行了,名字直观又好看,呵呵。还可以让整个项目的生命周期管理统一化,销毁一个对象你不再需要知道它是什么了。
重新发明delphi?
@风间星魂
什么嘛 都编译不过的说
c++越来越强大了,增加了很多很好的东西如lambda和正则。。。
整合所有其他语言先进的特性。以后编程就是开发效率,执行效率,语言学习曲线三者的最佳化。
我觉得不会加高门槛,现在的c++也很强大。
另外最后又亮了,居然有人会说方法论比技术好搞,真是无知者无谓。
@dngz
这个博客吞了我的\
@dngz
这个博客吞了T
在尖括号里加T就行了
貌似多了几分C#身影。。。。。
Lambda 表达式最重要的优点应该在于: 你不用跑到另外一个地方去实现一个函数,或者实现一个函数对象,这样代码很难看也不易管理。ptr_fun并不能解决代码到处跑的问题,你总得在某个地方显示的实现一个函数
……
越来越像python
C++0x 也有range for来代替foreach
比如上面第一个例子可以写成
for(char c : s) {
if (isupper(c))
Uppercase++;
}
lambda 也省了
对于 new A(); 虽然编译器行为不一样,但是标准是规定了的,这种叫做value-initialization
关于最后一点,我的看法是如果不是抱着要学会 C++ 全部 feature 的心态,那么其实门槛并不会很高。每个人或者团队其实都在使用 C++ 的一个子集。
C++ 很多 feature 是带来直接方便的,一看就会用的,这种可以毫不犹豫的拿来。像什么多线程,异步和内存模型之类的,本身就难,只要用不到不了解也没关系
1、c++不可能加垃圾回收的,加了垃圾回收那就不能再叫c++了;
2、c++继续朝着繁琐化的道路飞奔,也许入了歧途啊。在审美上,c和c++的关系,我觉得比较像明式家具和清式家具的区别,一个至简,一个繁琐。现在市场上明式家具可比清式家具值钱多了;
3、不过像闭包这些功能还是必要的,有很多应用场合,像GUI里边的消息处理,容器迭代,委托资源释放等。几乎所有现代语言都有这个特性。
4、auto也可以有,像for循环临时变量,不用再注意signed,unsigned,int32, int64,iter,一个auto全解决;
5、其他那些特性都是些小语法糖,好多都是为STL而加,日常开发中用途并不广泛,但无端增加了语言的复杂性。只有库适应语言,哪有语言适应库的。
从 Intel Thread Building Block 里第一次接触到 C++ 里的 lambda, 终于不用写一些只用一次的 functor 了.
花了3个星期把看了一遍,发现’哇’,stl真强大,就是不会用,或者想不到去用.