对2015.05技术沙龙中江南群主“任性”故事的探讨

五月份技术沙龙第二场演讲中,江南群主讲述的第二个“任性”故事的一个细节引发了一些争议。经小生仔细观察,三位讲师都有使用到相同的技术,但是实现的方法都有所不同,本文特来对这一个问题做一次详细的研究。

众所周知,在C++中实现反射是一件很棘手的事情。但是在确保没有类型退化(decay)和没有隐式类型转换的干扰下,是用模板元编程的一些技巧可以帮助我们完成一些目标。比如判断某个类型中是否含有某个成员函数,本文中的类成员函数仅指非静态成员函数

现实的基本思想是“一瞥”( catch a glimps of )这个成员函数。一瞥实际上是编译期的某种计算,而不会生成实际的代码。例如非求值运算符( Unevaluated Operator ),重载解析模板特化。非求值运算符在C++98下只有sizeof,到了C++11标准又扩充了decltype,typeid和noexcept三个操作符。先来看看C++98的实现。

由于sizeof只能对类型大小求值,因此C++98的版本的基本思想就是利用SFINAE和重载决议,配合sizeof来计算函数返回类型的大小,而获知哪一个test函数是决议的结果。如果是返回One类型的test,那么可以判断出这个类型T含有命名为foo,返回类型为void且有一个形参为void的成员函数。

这里有两个问题。1. 是否含有成员函数foo,单单只知道成员函数名是不够的,还必须知道它的完整签名,如返回值(暂不考虑cv修饰符)。2. 重载解析的规则里面有讲,相同的形参和不同返回类型的重载函数是有歧义的。因此,我们只需要知道形参就足够了,而不需要知道其完整的签名。

问题一很好解决,就是啰嗦点,除了提供函数名称之外,所有的形参和返回值类型一并提供。在c++98中,想做到这一点更啰嗦。我们需要提供形参数目从1到N每种版本的实现,具体实现多少个N由类库作者自行估量。

第二个问题,我们要寻求其他的实现方法了。因为指向非静态成员函数的指针(pointer to non-static memeber function)的签名有返回值类型,还有cv修饰符。基本思路是,假装我们要调用这个成员函数。先看看实现。

sizeof(((T*)0)->foo()),这一句就是假装去调用这个函数并尝试去计算函数返回值的类型的大小。由于sizeof是非求值运算符,因此没有生成运行期的空指针调用foo的代码。从这里就能看出sizeof操作符已经心有余而力不足了。首先sizeof将类型映射到了类型大小,丢失了类型信息;其次sizeof无法对void求值,这也是这个实现版本的硬伤。总之小生受能力所限,一直无法在cpp98版本中寻求比较好的实现方法。拜求得道大神之不吝赐教。

到了c++11的时代,情况好了许多。decltype这个非求值运算符的出现,让C++98中实现has_member_foo的第二种思路得以实现。先看看实现代码。

基本思想同样是SFINAE和重载解析,配合非求值运算符。只不过换成了delctype运算符。相比于sizeof运算符,decltype保留了非求值表达返回类型的完整信息,更重要的是decltype也能对void计算,不会出现sizeof(void)的尴尬。

深入细节,我们来看看C++11的实现版本是如何一瞥foo的调用的。通常,我们在描述数学问题的时候,总是使用假设。这里类比之,“假设”有一个类型的实例,且“假设”有可列的实参的全部实例,“尝试”使用这些实例调用函数foo,让decltype演绎这次假设的返回值结果!由于decltype是非求值运算符,所以计算中对象不需要真正的实例,同样函数只需要声明而不需要函数体。std::declval就是这样的一个函数。在std命名空间中就是返一个universal reference,并没有函数体。std::decltype只能用于非求值的计算当中。小生认为这种实现是最为简单的,如果使用指向非静态成员函数的指针来实现还要考虑返回值和cv修饰符,而这一种是完全不用考虑的。

接下来扩展一下思路。

拓展思路-另外的实现方法。文章的开头提及到的非求值的编译期计算,除了非求值运算符和重载解析外,还有一个模板特化。那么是否可以通过模板特化来实现本文的需求呢?先来看看基本思路。has_member_foo的泛型实现,应当返回false,继承std::false_type即可。在某种情况下的特化继承std::true_type!听起来是不是很美好?!实现细节。首先,为了简化问题,先不考虑成员函数有形参的情况。元函数has_member_foo的第一个参数是T,要判断的类型,应该是雷打不动的。而我们需要额外的一个参数,为特化的实现所用。

先举一个例子。还记得元函数enable_if配合模板特化的技巧吗?元函数template <typename T> struct size_integral;只计算整形类型的大小,其他类型不做任何计算。看看实现的代码。

当编译器尝试使用某个类型T去实例化size_integral的时候,会实例化所有的特化版本,并决议最佳的匹配。如果T是一个整形,那么特化版本会选择;如果T不是整形,那么特化版本实例化替换失败,根据SFINAE,会匹配泛型的实例化版本。

现在的问题是,针对本文的需求,我们需要一个元函数工具,使得传递任意可列个C++参数类型给它,它始终导出一个void. 为了迎合上面给出的例子的形式,小生才选择void。于是我们构建一个这样的工具,其实现如下。

也有了本文需求的另外一个实现

如果decltype能够成功推导出voider_t中的类型,那么决议就会选择这个more specialized的版本;反之,就会选择泛化的版本。实现是不是更优雅了?使用模板偏特化减少了使用重载解析造成的额外代码,至少小生谨以为然。

只可惜目前的编译器都不支持这种实现,原因是因为voider元函数没有使用任何模板参数,而被编译期做了优化。C++委员会也有一个提案来解决这个问题,详见CWG 1558, treatment of unused arguments in an alias template specialization. 其中的原委,小生还不是很清楚,望闻得道大神指教。

拓展思路-易用性。江南群主在这个故事中也提到了,这个元函数只能针对某一个成员函数,换一个成员函数要重新实现一个,中间有重复代码。因此有了使用宏来生成代码的实现版本,详见江南群主ppt. 可是江南群主的版本必须“两步走”!先得使用宏声明,再使用。有没有什么方法可以使用一个宏就能搞定?经过小生的测试,仅在msvc平台下还有一个小坑,把宏写在函数体内似乎有点问题,始终都找不到这个模板,不知道其他平台如何。因此一定要把宏写在函数体以外。此外,还需要考虑命名空间的问题。其实易用性的实现就目前的C++标准还是有些不够。

鄙文先写到这里。对于C++1y技术感兴趣的同学请加入我们社区,加入QQ群296561497讨论,让C++的世界更美好。

c++11实现一个简单的lexical_cast

boost中有一个lexical_cast可以用统一的方式来做基本类型之间的转换,比如字符串到数字,数字到字符串,bool和字符串及数字之间的相互转换。boost::lexical_cast的用法比较简单:


c++11中缺少lexical_cast方法,但是c++11已经提供了一些基本类型转换的方法,比如to_string, atoi, atof等等,但是我们不能通过一种通用的方式来做基本类型转换,因此我希望做一个类似boost的lexical_cast做基本类型的转换,这也是我们的c++社区的一个开发计划

由于c++11已经提供了一些便利的方法,我要做的事情就变得很简单了,就是把他们糅合在一起并提供一个统一的lexical_cast的方法即可。

实现思路也很简单,转换主要有这几种:1.数字到字符串的转换;2.字符串到数字的转换;3.bool与字符串的相互转换;4.数字转换为bool;具体的实现代码如下:

前后花了一个多小时,一个基本的类型转换类就完成了,再测试一下吧,测试代码:

测试结果:

应该用bind+function取代虚函数吗?

用bind+function取代虚函数在好几年前就有人提出了,曾引起广泛的讨论,有支持的有反对的,可能赞成的人占大多数。这个话题挺有趣,本来是作为技术沙龙的开放性话题来讨论的,由于时间关系并没有讨论。今天就来具体探讨一下这个问题,我将做两个实验来验证一下这两种做法,具体是实现两个模式:策略模式和责任链模式。我将分别用经典的虚函数和bind+function来实现这两个模式。通过这两个实验来得出我的结论。

实验一:策略模式的实现

1.虚函数方式实现策略模式

2.bind+function方式实现策略模式

测试代码:

bind+function取代虚函数的一个重要理由是虚函数带来了效率损失,bind+function效率更高,我做了一个性能测试, 分别调用10000000次来看耗时,发现虚函数比bind+function方式要快一些,无论是用标准库的bind还是boost的bind,都比虚函数方式要慢,所以说bind+function比虚函数性能更好是想当然,站不住脚的。接下来看第二个实验。

实验二:责任链模式的实现

1.虚函数方式实现责任链模式

2.bind+function方式实现责任链模式

测试代码:

bind+function实现责任链模式的关键代码在这里:

这几行代码通过assemble不断地往function链条中加function,最后调用的时候会从链条的第一个function开始调用。

bind+function取代虚函数的另外一个理由是松耦合,去除了继承的限制,方法的实现更加灵活,确实,低耦合确实是bind+function最大的优点,然而这个最大的优点也成了它最大的缺点,当需要替代的虚函数增多时,组装function的复杂度也在增加,太松散了导致代码也不够直观,代码的内聚性也变低了。比如上面责任链模式的实现,虚函数的实现明显比bind+function的实现要优雅。

结论

bind+function相比虚函数的实现在性能上并不占优,最大的优点是大大降低类之间的耦合度,缺点是太过于松散导致代码的内聚性和可读性降低。

bind+function适用的场景:

1.迫切需要接口和实现解耦;

2.需要解耦的接口很少。

满足这两种情况适合用bind+function,否则还是用虚函数更好。

近期社区开源项目开发计划

近期计划开发一些小的类库,主要是为了解决在实际工程中遇到的一些不便或问题:

1.实现一个定时器,不仅仅可以指定定时间隔,还支持指定日期运行。

2.实现一个不依赖于boost的lexical_cast的通用类型转换的类库。

约束:跨平台,不依赖于第三方库。

欢迎大家积极参与进来,社区最终会选择最优的实现。

用c++14做简单的lazy list

haskell中经常会提到惰性求值的这个特性,如其他大多数语言一样,cpp是热情求值的,但惰性求值在cpp中并不是不能做到,如:

变量v好像没什么意思,还得调用一下拿到值。但f就好玩儿了:auto fv = f(1,2,3); 并没有任何输出,因为lazyf将传入函数的环境给保存了下来,只有fv()才是真正地调用了原函数,就是说这个调用操作被延后了,即惰性。

不过好像还是没什么用呢…

考虑list,如果list中的元素是有规律的,即可以用一定的生成规则去创建,那其实我们并不需要后面的元素,或者说,后面的元素可以通过生成规则在需要的时候产生。嗯,看起来这个靠谱一点,怎么做呢?先来看看最简单的list结构:

从上看来,我们需要将value及next都lazy化,一个用于获取当前节点的值,一个用于在需要时生成下一个节点;同时发现为此需要一个更好的lazy工具,上面的lazyf第次在求值时都会重新计算一遍,这是没必要的:

如上所示,利用boolean init,原函数只会计算一次,类型为T()。

可以看到一个Lazy对象并不小,一个node上用上两个Lazy,另外考虑到拷贝很普遍,用这样的node就感觉很奢侈了。额…其实我完全没测试过…

无论如何,把整个node包起来动态分配会更好,这样引入一个中间类型来表示List,而内容用Lazy包装加以shared_ptr引用计数,List的拷贝问题就能最小化了,结构形如:

下面来看看ListNode的实现:

可以看到,由于把Lazy包装到List中去了,这个ListNode的实现跟没有Lazy一样。再来看看一开始的List是什么样子:

下面是几个最基本的函数的实现:

这些函数的命名都是延用haskell的。empty很自然,默认构造会创建一个空List,也即node == nullptr。head返回List中的第一个元素,如果为空List则异常。可以看到,只有当调用node->get()的时候,通过List构造函数传入的生成Node的函数才会被调用,而后,Node的value以及下一个节点的指针可见。tail()返回除开第一个元素的剩余元素组成的列表。

这两个函数稍稍复杂一点,首先看last,它返回列表中的最后一个元素,如果列表为空则异常。要注意的是,因为last必须遍历整个列表,因此对于无限列表(后述)来说会死循环。init更为复杂,它返回除开最后一个元素的前面所有元素组成的列表。由于列表的结构类似单链表,只能从前向后访问,因此必须检查接下来的两个节点。这里也看到了创建一个List的方法,即传入一个生成Node的生成函数,有三点需要注意:

1. List实为 List<T>,在类模板内可以简写

2. NodeType即ListNode<T>接受1个参数时,ListNode<T>::tail是一个空List,代表这就是最后一个节点了

3. init中看起来跟last一样递归调用了自身,会是一个eager(热情求值)操作,实则不然,t.init()处于lambda闭包之中,即生成(ListNode)函数之中,只有当init得到的List再次调用List::node->get()的时候,才会调用下一次的init以得到新的节点。

有了这些基本函数,就可以创建一些便捷函数来生成一些需要的List,包括无限长的列表,这个概念在haskell里经常拿出来安利,看看cpp怎么做吧:

printList与print用于测试呀什么的,print有点magic 0.0,不过对于群里了大神来说都不算什么了… 扯远了…

makeXXX便捷函数基本都能望文生义,要注意的是makeListInf,这家伙可以生成一个无限列表,惰性使然,所有的make都是惰性的。同时也可以看出这些函数都返回一个List,利用这样的方式,可以把大多数函数写成惰性的。

现在可以试试打印一个列表,输出1,10的奇数吧:

好像没什么特别的… 其实,我们还要一些些函数,比如,无限的List现在完全没法用。

这些函数的意思都很明确,最重要的是take,take让无限列表可用啦,你可以用惰性函数操作无限列表,最后调用take来得到你需要的列表元素数,另外take也是惰性的。

还有一个比较重要的地方,看到这几个函数里有用到auto l = *this; 然后绑定到lambda中,而不是用this,这是因为引入this到闭包,当对象本身已经不存在时,可能对node的引用已经为0,而lambda内部并不知道,所以绑定l即增加shared_ptr的计数很重要。来个take的例子:

接下来要来实现一些非常重要的函数,可以让这个List厉害起来,真的… 首先是concat,用于连接若干的List:

有了concat,我们可以连接若干的List,还可以连接无限列表,如:

当然无限列表也可以在前面,但是你就获取不到后面的数了 0.0

再来看select的实现,haskell中叫filter,但是俺觉得select比较帅:

select 接受一个返回boolean的函数用以判断是否选取这个值,可以看到实现中,如果不满足f,则直接递归,忽略当前的值v,来试试手,获取5个从10开始的(并)不能被2,3,5整除的数:

再来看一个重磅的家伙:map

map接受一个函数,将列表中所有的值映射到另一种值上,这说明输出类型并不一定是列表元素的类型。接下来是join:

join将List中的子List解开,并合并成一个新的list,看起来没什么用,实则厉害得很,下面来看怎么用map和join实现select:

是不是很炫,嘻嘻,下面来看foldl,很很很重要的一个函数:

foldl 接受一个 S(S, T) 类型的函数,和一个初始值,这个初始值最后会累计为结果。流程是:列表的每个值都应用于f,得到新的累计值s,然后用这个累计值和下一个列表值应用于f。

可以看出上面的foldl是eager的,原因是例如foldl一个整数List求和,一定会得到一个具体的数,这必然是eager的,但是从上面诸多函数可以看出如果返回一个List,那惰性自然就是可能的了。所以上面的模板参数中用到了enable_if,只将这个热情求值版本用于foldl结果为非List的情况,IsListType是个简单特化,就不说啦,下面来看惰性的foldl:

有foldl也就有foldr啦,一个是从左向右fold,一个是从右向左fold:

这里要说明两个问题,一是上面的fold的惰性版本其实是有缺陷的,表面上看来是惰性的无疑,但是一但对结果进行任何操作,包括take呀什么的,都会导致foldl一直求到最后一个节点,就是说上面的fold只能用于有限List。这个问题还不知道怎么解决,希望有大神告之一二。

二是fold函数必须指定返回类型,这也是一个顽固问题。首先std::funtion中的类型不能由模板自动推导,然后如果用funtion_traits(http://purecpp.org/?p=108#comment-18 木头大神的文)来获取可调用类型的话,又会导致泛型lambda不可用,因为泛型lambda的operator()为模板重载,不能decltype。但实际上理论讲,返回类型是已知的,可以推导…

撇开目前遇到的这两个问题,fold是很厉害的,先来用foldl实现个map吧:

最后来求个前45位fibnacci数:

感谢收看,文章有什么纰漏还望轻喷

对cpp11/14/17…感兴趣的可以加群: 296561497

群已被各种大大包围

从4行代码看右值引用

概述

  右值引用的概念有些读者可能会感到陌生,其实他和C++98/03中的左值引用有些类似,例如,c++98/03中的左值引用是这样的:

  这里的int&是对左值进行绑定(但是int&却不能绑定右值),相应的,对右值进行绑定的引用就是右值引用,他的语法是这样的A&&,通过双引号来表示绑定类型为A右值。通过&&我们就可以很方便的绑定右值了,比如我们可以这样绑定一个右值:

  这里我们绑定了一个右值0,关于右值的概念会在后面介绍。右值引用是C++11中新增加的一个很重要的特性,他主是要用来解决C++98/03中遇到的两个问题,第一个问题就是临时对象非必要的昂贵的拷贝操作,第二个问题是在模板函数中如何按照参数的实际类型进行转发。通过引入右值引用,很好的解决了这两个问题,改进了程序性能,后面将会详细介绍右值引用是如何解决这两个问题的。

  和右值引用相关的概念比较多,比如:右值、纯右值、将亡值、universal references、引用折叠、移动语义、move语义和完美转发等等。很多都是新概念,对于刚学习C++11右值引用的初学者来说,可能会觉得右值引用过于复杂,概念之间的关系难以理清。

右值引用实际上并没有那么复杂,其实是关于4行代码的故事,通过简单的4行代码我们就能清晰的理解右值引用相关的概念了。本文希望带领读者通过4行代码来理解右值引用相关的概念,理清他们之间的关系,并最终能透彻地掌握C++11的新特性右值引用。

四行代码的故事

1行代码的故事

  上面的这行代码很简单,从getVar()函数获取一个整形值,然而,这行代码会产生几种类型的值呢?答案是会产生两种类型的值,一种是左值i,一种是函数getVar()返回的临时值,这个临时值在表达式结束后就销毁了,而左值i在表达式结束后仍然存在,这个临时值就是右值,具体来说是一个纯右值,右值是不具名的。区分左值和右值的一个简单办法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。

  所有的具名变量或对象都是左值,而匿名变量则是右值,比如,简单的赋值语句:

  在这条语句中,是左值,字面量,就是右值。在上面的代码中,可以被引用,就不可以了。具体来说上面的表达式中等号右边的0是纯右值(prvalue),在C++11中所有的值必属于左值、将亡值、纯右值三者之一。比如,非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值。而将亡值是C++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。关于将亡值我们会在后面介绍,先看下面的代码:

  上面的代码中5是一个原始字面量, []{return 5;}是一个lambda表达式,都是属于纯右值,他们的特点是在表达式结束之后就销毁了。

  通过地行代码我们对右值有了一个初步的认识,知道了什么是右值,接下来再来看看第二行代码。

2行代码的故事

  第二行代码和第一行代码很像,只是相比第一行代码多了“&&”,他就是右值引用,我们知道左值引用是对左值的引用,那么,对应的,对右值的引用就是右值引用,而且右值是匿名变量,我们也只能通过引用的方式来获取右值。虽然第二行代码和第一行代码看起来差别不大,但是实际上语义的差别很大,这里,getVar()产生的临时值不会像第一行代码那样,在表达式结束之后就销毁了,而是会被“续命”,他的生命周期将会通过右值引用得以延续,和变量k的声明周期一样长。

右值引用的第一个特点

  通过右值引用的声明,右值又“重获新生”其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。让我们通过一个简单的例子来看看右值的生命周期。如代码清单1-1所示。

代码清单1-1

为了清楚的观察临时值,在编译时设置编译选项-fno-elide-constructors用来关闭返回值优化效果。

  输出结果:

  从上面的例子中可以看到,在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GetA()函数内部创建的对象返回出来构造一个临时对象产生的,另一次是在main函数中构造a对象产生的。第二次的destruct是因为临时对象在构造a对象之后就销毁了。如果开启返回值优化的话,输出结果将是:

construct: 1

destruct: 1

  可以看到返回值优化将会把临时对象优化掉,但这不是c++标准,是各编译器的优化规则。我们在回到之前提到的可以通过右值引用来延长临时右值的生命周期,如果上面的代码中我们通过右值引用来绑定函数返回值的话,结果又会是什么样的呢?在编译时设置编译选项-fno-elide-constructors。

  通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构,事实上,在c++98/03中,通过常量左值引用也经常用来做性能优化。上面的代码改成:

  const A& a = GetA();

  输出的结果和右值引用一样,因为常量左值引用是一个“万能”的引用类型,可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值,比如这样的写法是不对的:

  A& a = GetA();

  上面的代码会报一个编译错误,因为非常量左值引用只能接受左值。

右值引用的第二个特点

  右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值。比如下面的例子:

  var1类型为右值引用,但var1本身是左值,因为具名变量都是左值。

  关于右值引用一个有意思的问题是:T&&是什么,一定是右值吗?让我们来看看下面的例子:

  从上面的代码中可以看到,T&&表示的值类型不确定,可能是左值又可能是右值,这一点看起来有点奇怪,这就是右值引用的一个特点。

右值引用的第三个特点

  T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。

我们再回过头看上面的代码,对于函数template<typename T>void f(T&& t),当参数为右值10的时候,根据universal references的特点,t被一个右值初始化,那么t就是右值;当参数为左值x时,t被一个左值引用初始化,那么t就是一个左值。需要注意的是,仅仅是当发生自动类型推导(如函数模板的类型自动推导,或auto关键字)的时候,T&&才是universal references。再看看下面的例子:

  上面的例子中,paramuniversal reference,rhsTest&&右值引用,因为模版函数f发生了类型推断,而Test&&并没有发生类型推导,因为Test&&是确定的类型了。

  正是因为右值引用可能是左值也可能是右值,依赖于初始化,并不是一下子就确定的特点,我们可以利用这一点做很多文章,比如后面要介绍的移动语义和完美转发。

  这里再提一下引用折叠,正是因为引入了右值引用,所以可能存在左值引用与右值引用和右值引用与右值引用的折叠,C++11确定了引用折叠的规则,规则是这样的:

  • 所有的右值引用叠加到右值引用上仍然还是一个右值引用;
  • 所有的其他引用类型之间的叠加都将变成左值引用。

3行代码的故事

  这行代码实际上来自于一个类的构造函数,构造函数的一个参数是一个右值引用,为什么将右值引用作为构造函数的参数呢?在解答这个问题之前我们先看一个例子。如代码清单1-2所示。

代码清单1-2

  这个例子很简单,一个带有堆内存的类,必须提供一个深拷贝拷贝构造函数,因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题。如果不提供深拷贝的拷贝构造函数,上面的测试代码将会发生错误(编译选项-fno-elide-constructors),内部的m_ptr将会被删除两次,一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是所谓的指针悬挂问题。提供深拷贝的拷贝构造函数虽然可以保证正确,但是在有些时候会造成额外的性能损耗,因为有时候这种深拷贝是不必要的。比如下面的代码:

  上面代码中的GetA函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象a,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大的话,那么,这个拷贝构造的代价会很大,带来了额外的性能损失。每次都会产生临时变量并造成额外的性能损失,有没有办法避免临时变量造成的性能损失呢?答案是肯定的,C++11已经有了解决方法,看看下面的代码。如代码清单1-3所示。

代码清单1-3

  代码清单1-31-2相比只多了一个构造函数,输出结果表明,并没有调用拷贝构造函数,只调用了move construct函数,让我们来看看这个move construct函数:

  这个构造函数并没有做深拷贝,仅仅是将指针的所有者转移到了另外一个对象,同时,将参数对象a的指针置为空,这里仅仅是做了浅拷贝,因此,这个构造函数避免了临时变量的深拷贝问题。

  上面这个函数其实就是移动构造函数,他的参数是一个右值引用类型,这里的A&&表示右值,为什么?前面已经提到,这里没有发生类型推断,是确定的右值引用类型。为什么会匹配到这个构造函数?因为这个构造函数只能接受右值参数,而函数返回值是右值,所以就会匹配到这个构造函数。这里的A&&可以看作是临时值的标识,对于临时值我们仅仅需要做浅拷贝即可,无需再做深拷贝,从而解决了前面提到的临时变量拷贝构造产生的性能损失的问题。这就是所谓的移动语义,右值引用的一个重要作用是用来支持移动语义的。

  需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全。

  我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借助移动语义来优化性能呢,那该怎么做呢?事实上C++11为了解决这个问题,提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象资源的所有权从一个对象转移到另一个对象,只是转移,没有内存的拷贝,这就是所谓的move语义。如图1-1所示是深拷贝和move的区别。

1-1 深拷贝和move的区别

  再看看下面的例子:

  如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。他实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能。如果一个对象内部有较大的对内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。事实上,C++11中所有的容器都实现了移动语义,方便我们做性能优化。

  这里也要注意对move语义的误解,move实际上它并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用。如果是一些基本类型比如int和char[10]定长数组等类型,使用move的话仍然会发生拷贝(因为没有对应的移动构造函数)。所以,move对于含资源(堆内存或句柄)的对象来说更有意义。

4行代码故事

  C++11之前调用模板函数时,存在一个比较头疼的问题,如何正确的传递参数。比如:

都不能按照参数的本来的类型进行转发。

  C++11引入了完美转发:在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。C++11中的std::forward正是做这个事情的,他会按照参数的实际类型进行转发。看下面的例子:

  右值引用T&&是一个universal references,可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发。

  我们可以结合完美转发和移动语义来实现一个泛型的工厂函数,这个工厂函数可以创建所有类型的对象。具体实现如下:

  这个工厂函数的参数是右值引用类型,内部使用std::forward按照参数的实际类型进行转发,如果参数的实际类型是右值,那么创建的时候会自动匹配移动构造,如果是左值则会匹配拷贝构造。

总结

  通过4行代码我们知道了什么是右值和右值引用,以及右值引用的一些特点,利用这些特点我们才方便实现移动语义和完美转发。C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。

本文曾发表于《程序员》2015年1月刊。转载请注明出处。

后记:本文的内容主要来自于我在公司内部培训的一次课程,因为很多人对C++11右值引用搞不清或者理解得不深入,所以我觉得有必要拿出来分享一下,让更多的人看到,就整理了一下发到程序员杂志了,我相信读者看完之后对右值引用会有全面深入的了解。

弱弱的实现一个简单的Range

 

C++11改进我们的策略(Strategy)模式。

利用C++11的变参模板来改进我们的策略模式,使得在调用具体算法的时候可以接受任意参数,但是这里有个问题,即策略模式中的Strategy基类的虚函数不能为模板函数。在群主–江南的提示下,利用C++的CRTP(Curiously Recurring Template Prattern)可以用于虚函数无法应用的地方,如内联,或函数模板等。

代码如下:

完整代码已上传至群共享(c++1y boost 交流群 296561497)里。

C++技术沙龙主要内容

5月16日技术沙龙有三场主题演讲。

第一场演讲内容:C++11 Make life easier.

第二场演讲内容:玩转编译器,让编译器帮助我们构建“难用错”的类,在大工程批量修改时帮忙查错。

第三场演讲内容:C++的小魔法,使用 ML style pattern matching 轻松应对繁杂的业务逻辑

QQ图片20150508161749

精彩值得期待!!你还在等什么,快来报名吧。liantu

Copy Protected by Chetan's WP-Copyprotect.