Apache Flink 漫谈系列(03) - Watermark

实际问题(乱序)

在介绍Watermark相关内容之前我们先抛出一个具体的问题,在实际的流式计算中数据到来的顺序对计算结果的正确性有至关重要的影响,比如:某数据源中的某些数据由于某种原因(如:网络原因,外部存储自身原因)会有5秒的延时,也就是在实际时间的第1秒产生的数据有可能在第5秒中产生的数据之后到来(比如到Window处理节点).选具体某个delay的元素来说,假设在一个5秒的Tumble窗口(详见Window介绍章节),有一个EventTime是 11秒的数据,在第16秒时候到来了。图示第11秒的数据,在16秒到来了,如下图:
image

那么对于一个Count聚合的Tumble(5s)的window,上面的情况如何处理才能window2=4,window3=2 呢?

Apache Flink的时间类型

开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题,乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的,关于时间在Apache Flink中有如下三种时间类型,如下图:

image

  • ProcessingTime 
    是数据流入到具体某个算子时候相应的系统时间。ProcessingTime 有最好的性能和最低的延迟。但在分布式计算环境中ProcessingTime具有不确定性,相同数据流多次运行有可能产生不同的计算结果。
  • IngestionTime
    IngestionTime是数据进入Apache Flink框架的时间,是在Source Operator中设置的。与ProcessingTime相比可以提供更可预测的结果,因为IngestionTime的时间戳比较稳定(在源处只记录一次),同一数据在流经不同窗口操作时将使用相同的时间戳,而对于ProcessingTime同一数据在流经不同窗口算子会有不同的处理时间戳。
  • EventTime
    EventTime是事件在设备上产生时候携带的。在进入Apache Flink框架之前EventTime通常要嵌入到记录中,并且EventTime也可以从记录中提取出来。在实际的网上购物订单等业务场景中,大多会使用EventTime来进行数据计算。

开篇描述的问题和本篇要介绍的Watermark所涉及的时间类型均是指EventTime类型。

什么是Watermark

Watermark是Apache Flink为了处理EventTime 窗口计算提出的一种机制,本质上也是一种时间戳,由Apache Flink Source或者自定义的Watermark生成器按照需求Punctuated或者Periodic两种方式生成的一种系统Event,与普通数据流Event一样流转到对应的下游算子,接收到Watermark Event的算子以此不断调整自己管理的EventTime clock。 Apache Flink 框架保证Watermark单调递增,算子接收到一个Watermark时候,框架知道不会再有任何小于该Watermark的时间戳的数据元素到来了,所以Watermark可以看做是告诉Apache Flink框架数据流已经处理到什么位置(时间维度)的方式。 Watermark的产生和Apache Flink内部处理逻辑如下图所示: 
image

Watermark的产生方式

目前Apache Flink 有两种生产Watermark的方式,如下:

  • Punctuated - 数据流中每一个递增的EventTime都会产生一个Watermark。 
    在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。
  • Periodic - 周期性的(一定时间间隔或者达到一定的记录条数)产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时。

所以Watermark的生成方式需要根据业务场景的不同进行不同的选择。

Watermark的接口定义

对应Apache Flink Watermark两种不同的生成方式,我们了解一下对应的接口定义,如下:

  • Periodic Watermarks - AssignerWithPeriodicWatermarks
/**
 * Returns the current watermark. This method is periodically called by the
 * system to retrieve the current watermark. The method may return {@code null} to
 * indicate that no new Watermark is available.
 *
 * <p>The returned watermark will be emitted only if it is non-null and itsTimestamp
 * is larger than that of the previously emitted watermark (to preserve the contract of
 * ascending watermarks). If the current watermark is still
 * identical to the previous one, no progress in EventTime has happened since
 * the previous call to this method. If a null value is returned, or theTimestamp
 * of the returned watermark is smaller than that of the last emitted one, then no
 * new watermark will be generated.
 *
 * <p>The interval in which this method is called and Watermarks are generated
 * depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
 *
 * @see org.Apache.flink.streaming.api.watermark.Watermark
 * @see ExecutionConfig#getAutoWatermarkInterval()
 *
 * @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.
 */
 @Nullable
 Watermark getCurrentWatermark();
  • Punctuated Watermarks - AssignerWithPunctuatedWatermarks 
public interface AssignerWithPunctuatedWatermarks<T> extendsTimestampAssigner<T> {

/**
 * Asks this implementation if it wants to emit a watermark. This method is called right after
 * the {@link #extractTimestamp(Object, long)} method.
 *
 * <p>The returned watermark will be emitted only if it is non-null and itsTimestamp
 * is larger than that of the previously emitted watermark (to preserve the contract of
 * ascending watermarks). If a null value is returned, or theTimestamp of the returned
 * watermark is smaller than that of the last emitted one, then no new watermark will
 * be generated.
 *
 * <p>For an example how to use this method, see the documentation of
 * {@link AssignerWithPunctuatedWatermarks this class}.
 *
 * @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.
 */
 @Nullable
Watermark checkAndGetNextWatermark(T lastElement, long extractedTimestamp);
}

AssignerWithPunctuatedWatermarks 继承了TimestampAssigner接口 -TimestampAssigner

public interfaceTimestampAssigner<T> extends Function {

/**
 * Assigns aTimestamp to an element, in milliseconds since the Epoch.
 *
 * <p>The method is passed the previously assignedTimestamp of the element.
 * That previousTimestamp may have been assigned from a previous assigner,
 * by ingestionTime. If the element did not carry aTimestamp before, this value is
 * {@code Long.MIN_VALUE}.
 *
 * @param element The element that theTimestamp is wil be assigned to.
 * @param previousElementTimestamp The previous internalTimestamp of the element,
 *                                 or a negative value, if noTimestamp has been assigned, yet.
 * @return The newTimestamp.
 */
long extractTimestamp(T element, long previousElementTimestamp);
}

从接口定义可以看出,Watermark可以在Event(Element)中提取EventTime,进而定义一定的计算逻辑产生Watermark的时间戳。

Watermark解决如上问题

从上面的Watermark生成接口和Apache Flink内部对Periodic Watermark的实现来看,Watermark的时间戳可以和Event中的EventTime 一致,也可以自己定义任何合理的逻辑使得Watermark的时间戳不等于Event中的EventTime,Event中的EventTime自产生那一刻起就不可以改变了,不受Apache Flink框架控制,而Watermark的产生是在Apache Flink的Source节点或实现的Watermark生成器计算产生(如上Apache Flink内置的 Periodic Watermark实现), Apache Flink内部对单流或多流的场景有统一的Watermark处理。

回过头来我们在看看Watermark机制如何解决上面的问题,上面的问题在于如何将迟来的EventTime 位11的元素正确处理。要解决这个问题我们还需要先了解一下EventTime window是如何触发的? EventTime window 计算条件是当Window计算的Timer时间戳 小于等于 当前系统的Watermak的时间戳时候进行计算。 

  • 当Watermark的时间戳等于Event中携带的EventTime时候,上面场景(Watermark=EventTime)的计算结果如下:!image

 上面对应的DDL(Alibaba 企业版的Flink分支)定义如下:
 
CREATE TABLE source(
  ...,
  Event_timeTimeStamp,
  WATERMARK wk1 FOR Event_time as withOffset(Event_time, 0) 
) with (
  ...
);
 

  • 如果想正确处理迟来的数据可以定义Watermark生成策略为 Watermark = EventTime -5s, 如下:
    image

上面对应的DDL(Alibaba 内部的DDL语法,目前正在和社区讨论)定义如下: 
CREATE TABLE source(
  ...,
  Event_timeTimeStamp,
  WATERMARK wk1 FOR Event_time as withOffset(Event_time, 5000) 
) with (
  ...
);
上面正确处理的根源是我们采取了 延迟触发 window 计算 的方式正确处理了 Late Event. 与此同时,我们发现window的延时触发计算,也导致了下游的LATENCY变大,本例子中下游得到window的结果就延迟了5s.

多流的Watermark处理

在实际的流计算中往往一个job中会处理多个Source的数据,对Source的数据进行GroupBy分组,那么来自不同Source的相同key值会shuffle到同一个处理节点,并携带各自的Watermark,Apache Flink内部要保证Watermark要保持单调递增,多个Source的Watermark汇聚到一起时候可能不是单调自增的,这样的情况Apache Flink内部是如何处理的呢?如下图所示:

image

 
Apache Flink内部实现每一个边上只能有一个递增的Watermark, 当出现多流携带Eventtime汇聚到一起(GroupBy or Union)时候,Apache Flink会选择所有流入的Eventtime中最小的一个向下游流出。从而保证watermark的单调递增和保证数据的完整性.如下图:
 
image

 

小结

本节以一个流计算常见的乱序问题介绍了Apache Flink如何利用Watermark机制来处理乱序问题. 本篇内容在一定程度上也体现了EventTime Window中的Trigger机制依赖了Watermark(后续Window篇章会介绍)。Watermark机制是流计算中处理乱序,正确处理Late Event的核心手段。

转载

本文作者:金竹

来源:阿里云栖社区

来源链接:https://yq.aliyun.com/articles/686809

1、需要找到组织,多人一起学习一起交流。有兴趣的同学可加QQ群:732021751。

2. 通过看书学习,很遗憾,Flink这块目前还没有系统、实战性强的书出来,预计还得再等等。

3. 看Flink老鸟的分享视频,这个确实是一个可选方案,适合想快速学好Flink并积累一些项目经验的同学。目前各大IT学习平台比较热门的应该要数《Flink大数据项目实战》这套视频啦,感兴趣的 -> 戳此链接

展开阅读全文

《C++0x漫谈系列之:右值引用

04-17

右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升… rnrn  Move语意rnrn  返回值效率问题——返回值优化((N)RVO)——mojo设施——workaround——问题定义——Move语意——语言支持rnrn  大猴子Howard Hinnant写了一篇挺棒的tutorial(a.k.a. 提案N2027),此外最初的关于rvalue-reference的若干篇提案的可读性也相当强。因此要想了解rvalue-reference的话,或者去看C++标准委员会网站上的系列提案(见文章末尾的参考文献)。或者阅读本文。rnrn  源起rnrn  《大史记》总看过吧?rnrn  故事,素介个样子滴…一天,小嗖风风的吹着,在一个伸手不见黑夜的五指…rnrn  我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…rnrn  设想这样一段代码(既然大同小异,就直接从Andrei那篇著名的文章里面拿来了):rnrnstd::vector v = readFile(); rnrnrn  readFile()的定义是这样的:rnrnstd::vector readFile()rnrnstd::vector retv;rn… // fill retvrnreturn retv;rn rnrnrn  这段代码低效的地方在于那个返回的临时对象。一整个vector得被拷贝一遍,仅仅是为了传递其中的一组int,当v被构造完毕之后,这个临时对象便烟消云散。rnrn  这完全是公然的浪费!rnrn  更糟糕的是,原则上讲,这里有两份浪费。一,retv(retv在readFile()结束之后便烟消云散)。二,返回的临时对象(返回的临时变量在v拷贝构造完毕之后也随即香消玉殒)。不过呢,对于上面的简单代码来说,大部分编译器都已经能够做到优化掉这两个对象,直接把那个retv创建到接受返回值的对象,即v中去。rnrn  实际上,临时对象的效率问题一直是C++中的一个被广为诟病的问题。这个问题是如此的著名,以至于标准不惜牺牲原本简洁的拷贝语意,在标准的12.8节悍然下诏允许优化掉在函数返回过程中产生的拷贝(即便那个拷贝构造函数有副作用也在所不惜!)。这就是所谓的“Copy Elision”。rnrn  为什么(N)RVO((Named) Return Value Optimization)几乎形同虚设rnrn  还是按照Andrei的说法,只要readFile()改成这样:rnrn… readFile()rnrnif(/* err condition */) return std::vector();rnif(/* yet another err condition */) return std::vector(1, 0);rnstd::vector retv;rn… // fill retvrnreturn retv;rn rnrnrn  出现这种情况,编译器一般都会乖乖放弃优化。rnrn  但对编译器来说这还不是最郁闷的一种情况,最郁闷的是:rnrnstd::vector v;rnv = readFile(); // assignment, not copy construction rnrnrn  这下由拷贝构造,变成了拷贝赋值。眼睛一眨,老母鸡变鸭。编译器只能缴械投降。因为标准只允许在拷贝构造的情况下进行(N)RVO。rnrn  为什么库方案也不是生意经rnrn  C++鬼才Andrei Alexandrescu以对C++标准的深度挖掘和利用著名,早在03年的时候(当时所谓的临时变量效率问题已经在新闻组上闹了好一阵子了,相关的语言级别的解决方案也已经在02年9月份粉墨登场)就在现有标准(C++98)下硬是折腾出了一个能100%解决问题的方案来。rnrn  Andrei把这个框架叫做mojo,就像一层爽身粉一样,把它往现有类上面一洒,嘿嘿…猜怎么着,不,不是“痱子去无踪”:P,是该类型的临时对象效率问题就迎刃而解了!rnrn  Mojo的唯一的问题就是使用方法过于复杂。这个复杂度,很大程度上来源于标准中的一个措辞问题(C++标准就是这样,鬼知道哪个角落的一句话能够带出一个brilliant的解决方案来,同时,鬼知道哪个角落的一句话能够抹杀一个原本简洁的解决方案)。这个问题就是我前面提到过的8.5.3问题,目前已经由core language issue 391解决。rnrn  对于库方案来说,解决问题固然是首要的。但一个侵入性的,外带使用复杂性的方案必然是走不远的。因此虽然大家都不否认mojo是一个天才的方案,但实际使用中难免举步维艰。这也是为什么mojo并没有被工业化的原因。rnrn  为什么改用引用传参也等于痴人说梦rnrnvoid readFile(vector& v) … // fill v rnrnrn  这当然可以。rnrn  但是如果遇到操作符重载呢?rnrnstring operator+(string const& s1, string const& s2); rnrnrn  而且,就算是对于readFile,原先的返回vector的版本支持rnrnBOOST_FOREACH(int i, readFile())rn… // do sth. with irn rnrnrn  改成引用传参后,原本优雅的形式被破坏了,为了进行以上操作不得不引入一个新的名字,这个名字的存在只是为了应付被破坏的形式,一旦foreach操作结束它短暂的生命也随之结束:rnrnvector v;rnreadFile(v);rnBOOST_FOREACH(int I, v)rnrnrn// v becomes useless here rnrnrn  还有什么问题吗?自己去发现吧。总之,利用引用传参是一个解决方案,但其能力有限,而且,其自身也会带来一些其它问题。终究不是一个优雅的办法。rnrn  问题是什么rnrn  《你的灯亮着吗?》里面漂亮地阐述了定义“问题是什么”的重要性。对于我们面临的临时对象的效率问题,这个问题同样重要。rnrn  简而言之,问题可以描述为:rnrn  C++没有区分copy和move语意。rnrn  什么是move语意?记得auto_ptr吗?auto_ptr在“拷贝”的时候其实并非严格意义上的拷贝。“拷贝”是要保留源对象不变,并基于它复制出一个新的对象出来。但auto_ptr的“拷贝”却会将源对象“掏空”,只留一个空壳——一次资源所有权的转移。rnrn  这就是move。rnrn  Move语意的作用——效率优化rnrn  举个具体的例子,std::string的拷贝构造函数会做两件事情:一,根据源std::string对象的大小分配一段大小适当的缓冲区。二,将源std::string中的字符串拷贝过来。rnrn// just for illustrating the idea, not the actual implementationrnrnstring::string(const string& o)rnrnthis->buffer_ = new buffer[o.length() + 1];rncopy(o.begin(), o.end(), buffer_);rn rnrnrn  但是假设我们知道o是一个临时对象(比如是一个函数的返回值),即o不会再被其它地方用到,o的生命期会在它所处的full expression的结尾结束的话,我们便可以将o里面的资源偷过来:rnrnstring::string(temporary string& o)rnrn// since o is a temporary, we can safely steal its resources without causing any problemrnthis->buffer_ = o.buffer_;rno.buffer_ = 0;rn rnrnrn  这里的temporary是一个捏造的关键字,其作用是使该构造函数区分出临时对象(即只有当参数是一个临时的string对象时,该构造函数才被调用)。rnrn  想想看,如果存在这样一个move constructor(搬移式构造函数)的话,所有源对象为临时对象的拷贝构造行为都可以简化为搬移式(move)构造。对于上面的string例子来说,move和copy construction之间的效率差是节省了一次O(n)的分配操作,一次O(n)的拷贝操作,一次O(1)的析构操作(被拷贝的那个临时对象的析构)。这里的效率提升是显而易见且显著的。rnrn  最后,要实现这一点,只需要我们具有判断左值右值的能力(比如前面设想的那个temporary关键字),从而针对源对象为临时对象的情况进行“偷”资源的行动。rnrn  Move语意的作用——使能(enabling)rnrn  再举一个例子,std::fstream。fstream是不可拷贝的(实际上,所有的标准流对象都是不可拷贝的),因而我们只能通过引用来访问一开始建立的那个流对象。但是,这种办法有一个问题,如果我们要从一个函数中返回一个流对象出来就不行了:rnrn// how do we make this happen?rnstd::fstream createStream()rn … rnrnrn  当然,你可以用auto_ptr来解决这个问题,但这就使代码非常笨拙且难以维护。rnrn  但如果fstream是moveable的,以上代码就是可行的了。所谓“moveable”即是指(当源对象是临时对象时)在对象拷贝语法之下进行的实际动作是像auto_ptr那样的资源所有权转移:源对象被掏空,所有资源都被转移到目标对象中——好比一次搬家(move)。move操作之后,源对象虽然还有名有姓地存在着,但实际上其“实质”(内部拥有的资源)已经消失了,或者说,源对象从语意上已经消失了。rnrn  对于moveable但并非copyable的fstream对象来说,当发生一次move时(比如在上面的代码中,当一个局部的fstream对象被move出createStream()函数时),不会出现同一对象的两个副本,取而代之的是,move的源对象的身份(Identity)消失了,这个身份由返回的临时fstream对象重新持有。也就是说,fstream的唯一性(不可拷贝性——non-copyable)得到了尊重。rnrn  你可能会问,那么被搬空了的那个源对象如果再被使用的话岂不是会引发问题?没错。这就是为什么我们应该仅当需要且可以去move一个对象的时候去move它,比如在函数的最后一行(return)语句中将一个局部的vector对象move出来(return std::move(v)),由于这是最后一行语句,所以后面v不可能再被用到,对它来说所剩下的操作就是析构,因此被掏空从语意上是完全恰当的。rn  在先前的那个例子中rnrnvector v = readFile(); rnrnrn  有了move语意的话,readFile就可以简单的改成:rnrnstd::vector readFile()rnrnstd::vector retv;rn… // fill retvrnreturn std::move(retv); // move retv outrn rnrnrn  std::move以后再介绍。目前你只要知道,std::move就可以把retv掏空,即搬移出去,而搬家的最终目的地是v。这样的话,从内存分配的角度讲,只有retv中进行的内存分配,在从retv到返回的临时对象,再从后者到目的地v的“move”过程中,没有任何的内存分配(我是指vector内的缓冲区分配),取而代之的是,先是retv内的缓冲区被“转移”到返回值临时对象中,然后再从临时对象中转移到v中。相比于以前的两次拷贝而言,两次move操作节省了多少工作量呢?节省了两次new操作两次delete操作,还有两次O(n)的拷贝操作,这些操作整体的代价正比于retv这个vector的大小。难怪人们说临时对象效率问题是C++的肿瘤(wart)之一,难怪C++标准都要不惜代价允许(N)RVO。rnrn  如何支持move语意rnrn  根据前面的介绍,你想必已经知道。实现move语意的最关键环节在于能够在编译期区分左值右值(也就是说识别出临时对象)。rnrn  现在,回忆一下,在文章的开头我曾经提到:rnrn  我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…rnrn  为什么这么说?rnrn  现行标准(C++03)下的方案rnrn  要想区分左值右值,只有通过重载:rnrnvoid foo(X const&);rnvoid foo(X&); rnrnrn  这样的重载显然是行不通的。因为X const&会把non-const临时对象一并吞掉。rnrn  这种做法的问题在于。X&是一个non-const引用,它只能接受non-const左值。然而,C++里面的值一共有四种组合:rnrn  const non-constrn  lvaluern  rvaluernrn  常量性(const-ness)与左值性(lvalue-ness)是正交的。rnrn  non-const引用只能绑定到其中的一个组合,即non-const lvalue。还剩下const左值,const右值,以及我们最关心的——non-const右值。而只有最后一种——non-const右值——才是可以move的。rnrn  剩下的问题便是如何设计重载函数来搞定const左值和const右值。使得最后只留下non-const右值。rnrn  所幸的是,我们可以借助强大的模板参数推导机制:rnrn// catch non-const lvaluesrnvoid foo(X&);rn// catch const lvalues and const rvaluesrntemplaternvoid foo(T&, enable_if_same::type* = 0);rnvoid foo( /* what goes here? */); rnrnrn  注意,第二个重载负责接受const左值和const右值。经过第一第二个foo重载之后剩下来的便是non-const rvalue了。rnrn  问题是,我们怎么捕获这些non-const rvalue呢?根据C++03,const-const rvalue只能绑定到const引用。但如果我们用const引用的话,就会越俎代庖把const左右值一并接受了(因为在模板函数(第二个重载)和非模板函数(第三个重载)之间编译器总是会偏好非模板)。rnrn  那除了用const引用,难道还有什么办法来接受一个non-const rvalue吗?rnrn  有。rnrn  假设你的类型为X,那么只要在X里面加入一点料:rnrnstruct ref_xrnrnref_x(X* p) : p_(p) rnX* p_;rn;rnstruct Xrnrn// original stuffrn…rn// added stuff, for move semanticrnoperator ref_x()rnrnreturn ref_x(this);rnrn; rnrnrn  这样,我们的第三个重载函数便可以写成:rnrnvoid foo(ref_x rx); // accept non-const temporaries only! rnrnrn  Bang! 我们成功地在C++03下识别出了moveable的non-const临时对象。不过前提是必须得在moveable的类型里加入一些东西。这也正是该方案的最大弊病——它是侵入式的(姑且不说它利用了语言的阴暗角落,并且带来了很大的编码复杂度)。rnrn  C++09的方案rnrn  实际上,刚才讲的这个利用重载的方案做成库便是Andrei的mojo框架。mojo框架固然精巧,但复杂性太大,使用成本太高,不够优雅直观。所以语言级别的支持看来是必然选择(后面你还会看到,为了支持move语意而引入的新的语言特性同时还支持了另一个广泛的问题——完美转发)。rnrn  C++03之所以让人费神就是因为它没有一个引用类型来绑定到右值,而是用const左值引用来替代,事实证明这个权宜之计并不是长远之道,时隔10年,终归还是要健全引用的左右值语意。rnrn  C++09加入一个新的引用类型——右值引用。右值引用的特点是优先绑定到右值。其语法是&&(注意,不读作“引用的引用”,读作“右值引用”)。有了右值引用,我们前面的方案便可以简单的修改为:rnrnvoid foo(X const& x);rnvoid foo(X&& x); rnrnrn  这样一来,左值以及const右值都被绑定到了第一个重载版本。剩下的non-const右值被绑定到第二个重载版本。rnrn  对于你的moveable的类型X,则是这样:rnrnstruct XrnrnX();rnX(X const& o); // copy constructorrnX(X&& o); // move constructorrn;rnrnX source();rnX x = source(); // #1 rnrnrn  在#1处,调用的将会是X::X(X&& o),即所谓的move constructor,因为source()返回的是一个临时对象(non-const右值),重载决议会选中move constructor。rnrn阅读:44rn 论坛

没有更多推荐了,返回首页