社区应用 最新帖子 精华区 社区服务 会员列表 统计排行 银行

  • 27392阅读
  • 45回复

C++0x2011年8月10日获通过正式成为国际标准

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 10楼 发表于: 2011-08-15
《C++0x漫谈》系列之:右值引用(或“move语意与完美转发”)
右值引用导言

右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升…

Move语意

返回值效率问题——返回值优化((N)RVO)——mojo设施——workaround——问题定义——Move语意——语言支持

大猴子Howard Hinnant写了一篇挺棒的tutorial(a.k.a. 提案N2027),此外最初的关于rvalue-reference的若干篇提案的可读性也相当强。因此要想了解rvalue-reference的话,或者去看C++标准委员会网站上的系列提案(见文章末尾的参考文献)。或者阅读本文。

源起
《大史记》总看过吧?

故事,素介个样子滴…一天,小嗖风风的吹着,在一个伸手不见黑夜的五指…(哎哟,谁人扔滴板砖?!%$@)

我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…

设想这样一段代码(既然大同小异,就直接从Andrei那篇著名的文章里面拿来了):

std::vector<int> v = readFile();

readFile()的定义是这样的:

std::vector<int> readFile()
{
  std::vector<int> retv;
  … // fill retv
  return retv;
}

这段代码低效的地方在于那个返回的临时对象。一整个vector得被拷贝一遍,仅仅是为了传递其中的一组int,当v被构造完毕之后,这个临时对象便烟消云散。

这完全是公然的浪费!

更糟糕的是,原则上讲,这里有两份浪费。一,retv(retv在readFile()结束之后便烟消云散)。二,返回的临时对象(返回的临时变量在v拷贝构造完毕之后也随即香消玉殒)。不过呢,对于上面的简单代码来说,大部分编译器都已经能够做到优化掉这两个对象,直接把那个retv创建到接受返回值的对象,即v中去。

实际上,临时对象的效率问题一直是C++中的一个被广为诟病的问题。这个问题是如此的著名,以至于标准不惜牺牲原本简洁的拷贝语意,在标准的12.8节悍然下诏允许优化掉在函数返回过程中产生的拷贝(即便那个拷贝构造函数有副作用也在所不惜!)。这就是所谓的“Copy Elision”。

为什么(N)RVO((Named) Return Value Optimization)几乎形同虚设
还是按照Andrei的说法,只要readFile()改成这样:

… readFile()
{
if(/* err condition */) return std::vector<int>();
if(/* yet another err condition */) return std::vector<int>(1, 0);
std::vector<int> retv;
… // fill retv
return retv;
}

出现这种情况,编译器一般都会乖乖放弃优化。

但对编译器来说这还不是最郁闷的一种情况,最郁闷的是:

std::vector<int> v;
v = readFile(); // assignment, not copy construction

这下由拷贝构造,变成了拷贝赋值。眼睛一眨,老母鸡变鸭。编译器只能缴械投降。因为标准只允许在拷贝构造的情况下进行(N)RVO。

为什么库方案也不是生意经
C++鬼才Andrei Alexandrescu以对C++标准的深度挖掘和利用著名,早在03年的时候(当时所谓的临时变量效率问题已经在新闻组上闹了好一阵子了,相关的语言级别的解决方案也已经在02年9月份粉墨登场)就在现有标准(C++98)下硬是折腾出了一个能100%解决问题的方案来。

Andrei把这个框架叫做mojo,就像一层爽身粉一样,把它往现有类上面一洒,嘿嘿…猜怎么着,不,不是“痱子去无踪”:P,是该类型的临时对象效率问题就迎刃而解了!

Mojo的唯一的问题就是使用方法过于复杂。这个复杂度,很大程度上来源于标准中的一个措辞问题(C++标准就是这样,鬼知道哪个角落的一句话能够带出一个brilliant的解决方案来,同时,鬼知道哪个角落的一句话能够抹杀一个原本简洁的解决方案)。这个问题就是我前面提到过的8.5.3问题,目前已经由core language issue 391解决。

对于库方案来说,解决问题固然是首要的。但一个侵入性的,外带使用复杂性的方案必然是走不远的。因此虽然大家都不否认mojo是一个天才的方案,但实际使用中难免举步维艰。这也是为什么mojo并没有被工业化的原因。

为什么改用引用传参也等于痴人说梦
void readFile(vector<int>& v){ … // fill v }

这当然可以。

但是如果遇到操作符重载呢?

string operator+(string const& s1, string const& s2);

而且,就算是对于readFile,原先的返回vector的版本支持

BOOST_FOREACH(int i, readFile()){
  … // do sth. with i
}

改成引用传参后,原本优雅的形式被破坏了,为了进行以上操作不得不引入一个新的名字,这个名字的存在只是为了应付被破坏的形式,一旦foreach操作结束它短暂的生命也随之结束:

vector<int> v;
readFile(v);

BOOST_FOREACH(int I, v){
}

// v becomes useless here

还有什么问题吗?自己去发现吧。总之,利用引用传参是一个解决方案,但其能力有限,而且,其自身也会带来一些其它问题。终究不是一个优雅的办法。

问题是什么
《你的灯亮着吗?》里面漂亮地阐述了定义“问题是什么”的重要性。对于我们面临的临时对象的效率问题,这个问题同样重要。

简而言之,问题可以描述为:

C++没有区分copy和move语意。

什么是move语意?记得auto_ptr吗?auto_ptr在“拷贝”的时候其实并非严格意义上的拷贝。“拷贝”是要保留源对象不变,并基于它复制出一个新的对象出来。但auto_ptr的“拷贝”却会将源对象“掏空”,只留一个空壳——一次资源所有权的转移。

这就是move。

Move语意的作用——效率优化
举个具体的例子,std::string的拷贝构造函数会做两件事情:一,根据源std::string对象的大小分配一段大小适当的缓冲区。二,将源std::string中的字符串拷贝过来。

// just for illustrating the idea, not the actual implementation
string::string(const string& o)
{
this->buffer_ = new buffer[o.length() + 1];
copy(o.begin(), o.end(), buffer_);
}

但是假设我们知道o是一个临时对象(比如是一个函数的返回值),即o不会再被其它地方用到,o的生命期会在它所处的full expression的结尾结束的话,我们便可以将o里面的资源偷过来:

string::string(temporary string& o)
{
// since o is a temporary, we can safely steal its resources without causing any problem

this->buffer_ = o.buffer_;
o.buffer_ = 0;
}

这里的temporary是一个捏造的关键字,其作用是使该构造函数区分出临时对象(即只有当参数是一个临时的string对象时,该构造函数才被调用)。

想想看,如果存在这样一个move constructor(搬移式构造函数)的话,所有源对象为临时对象的拷贝构造行为都可以简化为搬移式(move)构造。对于上面的string例子来说,move和copy construction之间的效率差是节省了一次O(n)的分配操作,一次O(n)的拷贝操作,一次O(1)的析构操作(被拷贝的那个临时对象的析构)。这里的效率提升是显而易见且显著的。

最后,要实现这一点,只需要我们具有判断左值右值的能力(比如前面设想的那个temporary关键字),从而针对源对象为临时对象的情况进行“偷”资源的行动。

Move语意的作用——使能(enabling)
再举一个例子,std::fstream。fstream是不可拷贝的(实际上,所有的标准流对象都是不可拷贝的),因而我们只能通过引用来访问一开始建立的那个流对象。但是,这种办法有一个问题,如果我们要从一个函数中返回一个流对象出来就不行了:

// how do we make this happen?
std::fstream createStream()
{ … }

当然,你可以用auto_ptr来解决这个问题,但这就使代码非常笨拙且难以维护。

但如果fstream是moveable的,以上代码就是可行的了。所谓“moveable”即是指(当源对象是临时对象时)在对象拷贝语法之下进行的实际动作是像auto_ptr那样的资源所有权转移:源对象被掏空,所有资源都被转移到目标对象中——好比一次搬家(move)。move操作之后,源对象虽然还有名有姓地存在着,但实际上其“实质”(内部拥有的资源)已经消失了,或者说,源对象从语意上已经消失了。

对于moveable但并非copyable的fstream对象来说,当发生一次move时(比如在上面的代码中,当一个局部的fstream对象被move出createStream()函数时),不会出现同一对象的两个副本,取而代之的是,move的源对象的身份(Identity)消失了,这个身份由返回的临时fstream对象重新持有。也就是说,fstream的唯一性(不可拷贝性——non-copyable)得到了尊重。

你可能会问,那么被搬空了的那个源对象如果再被使用的话岂不是会引发问题?没错。这就是为什么我们应该仅当需要且可以去move一个对象的时候去move它,比如在函数的最后一行(return)语句中将一个局部的vector对象move出来(return std::move(v)),由于这是最后一行语句,所以后面v不可能再被用到,对它来说所剩下的操作就是析构,因此被掏空从语意上是完全恰当的。

最初的例子——完美解决方案
在先前的那个例子中

vector<int> v = readFile();

有了move语意的话,readFile就可以简单的改成:

std::vector<int> readFile()
{
std::vector<int> retv;
… // fill retv
return std::move(retv); // move retv out
}

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。

如何支持move语意
根据前面的介绍,你想必已经知道。实现move语意的最关键环节在于能够在编译期区分左值右值(也就是说识别出临时对象)。

现在,回忆一下,在文章的开头我曾经提到:

我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…

为什么这么说?

现行标准(C++03)下的方案
要想区分左值右值,只有通过重载:

void foo(X const&);
void foo(X&);

这样的重载显然是行不通的。因为X const&会把non-const临时对象一并吞掉。

这种做法的问题在于。X&是一个non-const引用,它只能接受non-const左值。然而,C++里面的值一共有四种组合:

        const    non-const
lvalue
rvalue

常量性(const-ness)与左值性(lvalue-ness)是正交的。

non-const引用只能绑定到其中的一个组合,即non-const lvalue。还剩下const左值,const右值,以及我们最关心的——non-const右值。而只有最后一种——non-const右值——才是可以move的。

剩下的问题便是如何设计重载函数来搞定const左值和const右值。使得最后只留下non-const右值。

所幸的是,我们可以借助强大的模板参数推导机制:

// catch non-const lvalues
void foo(X&);

// catch const lvalues and const rvalues
template<typename T>
void foo(T&, enable_if_same<T, const X>::type* = 0);

void foo( /* what goes here? */);

注意,第二个重载负责接受const左值和const右值。经过第一第二个foo重载之后剩下来的便是non-const rvalue了。

问题是,我们怎么捕获这些non-const rvalue呢?根据C++03,const-const rvalue只能绑定到const引用。但如果我们用const引用的话,就会越俎代庖把const左右值一并接受了(因为在模板函数(第二个重载)和非模板函数(第三个重载)之间编译器总是会偏好非模板)。

那除了用const引用,难道还有什么办法来接受一个non-const rvalue吗?

有。

假设你的类型为X,那么只要在X里面加入一点料:

struct ref_x
{
ref_x(X* p) : p_(p) {}
X* p_;
};

struct X
{
// original stuff


// added stuff, for move semantic
operator ref_x()
{
return ref_x(this);
}
};

这样,我们的第三个重载函数便可以写成:

void foo(ref_x rx); // accept non-const temporaries only!

Bang! 我们成功地在C++03下识别出了moveable的non-const临时对象。不过前提是必须得在moveable的类型里加入一些东西。这也正是该方案的最大弊病——它是侵入式的(姑且不说它利用了语言的阴暗角落,并且带来了很大的编码复杂度)。

C++09的方案
实际上,刚才讲的这个利用重载的方案做成库便是Andrei的mojo框架。mojo框架固然精巧,但复杂性太大,使用成本太高,不够优雅直观。所以语言级别的支持看来是必然选择(后面你还会看到,为了支持move语意而引入的新的语言特性同时还支持了另一个广泛的问题——完美转发)。

C++03之所以让人费神就是因为它没有一个引用类型来绑定到右值,而是用const左值引用来替代,事实证明这个权宜之计并不是长远之道,时隔10年,终归还是要健全引用的左右值语意。

C++09加入一个新的引用类型——右值引用。右值引用的特点是优先绑定到右值。其语法是&&(注意,不读作“引用的引用”,读作“右值引用”)。有了右值引用,我们前面的方案便可以简单的修改为:

void foo(X const& x);
void foo(X&& x);

这样一来,左值以及const右值都被绑定到了第一个重载版本。剩下的non-const右值被绑定到第二个重载版本。

对于你的moveable的类型X,则是这样:

struct X
{
X();
X(X const& o); // copy constructor
X(X&& o); // move constructor
};

X source();

X x = source(); // #1

在#1处,调用的将会是X::X(X&& o),即所谓的move constructor,因为source()返回的是一个临时对象(non-const右值),重载决议会选中move constructor。


右值引用导言

右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升…


完美转发

完美转发问题——不完美解决方案——模板参数推导规则——完美转发

动机
关于“完美转发”这个特性,其实提案N1385已经讲得非常清楚了,诸位可以直接去看N1385,如果实在还是觉得迷糊就再回来听我唠叨吧:-)

在泛型编码中经常出现的一个问题是(这个问题在实际中出现的场景很多,我们留到文章末尾再提,目前我们将这个特定的问题先提取孤立出来考虑):

如何将一组参数原封不动地转发给另一个函数

注意,这里所谓“原封不动”就是指,如果参数是左值,那么转发给的那个函数也要接受到一个左值,如果参数是右值,那么后者要接受到一个右值;同理,如果参数是const的,那么转发给的那个函数也要接受到一个const的值,如果是non-const的,那么后者也要接受到一个non-const的值。

总之一句话:

保持参数的左值/右值、const/non-const属性不变

听上去很简单吗?不妨试试看。

(不完美的)解决方案
假设我们要写一个泛型转发函数f,f要将它的参数原封不动地转发给g(不管g的参数类型是什么):

template<typename T>
void f(/*what goes here?*/ t)
{
g(t);
}

上面的代码中,f的参数t的类型是什么?T?T&?const T&?

我们一个个来分析。

Value
如果t的类型是T,即:

// take #1
template<typename T>
void f(T t)
{
g(t);
}

那么很显然,不能满足如下情况:

void g(int& i) { ++i; }

int myInt = 0;

f(myInt); // error, the value g() has incremented is a local value(a.k.a. f’s argument ‘t’)

即,不能将左值转发为左值。

Const&
如果t的类型为const T&,即:

// take #2
template<typename T>
void f(const T& t)
{
g(t);
}

则刚才的情况还是不能满足。因为g接受的参数类型为non-const引用。

Non-const&
那如果t的类型是T&呢?

// take #3
template<typename T>
void f(T& t)
{
g(t);
}

我们惊讶地发现,这时,如果参数是左值,那么不管是const左值还是non-const左值,f都能正确转发,因为对于const左值,T将会被推导为const U(U为参数的实际类型)。并且,对于const右值,f也能正确转发(因为const引用能够绑定到右值)。只有对non-const右值不能完美转发(因为这时T&会被推导为non-const引用,而后者不能绑定到右值)。

即四种情况里面有三种满足了,只有以下这种情况失败:

void g(const int& i);

int source();

f(source()); // error

如果f是完美转发的话,那么f(source())应该完全等价于g(source()),后者应该通过编译,因为g是用const引用来接受参数的,后者在面对一个临时的int变量的时候应该完全能够绑定。

而实际上以上代码却会编译失败,因为f的参数是T&,当面对一个non-const的int型右值(source()的返回值)时,会被推导为int&,而non-const引用不能绑定到右值。

好,现在的问题就变成,如何使得non-const右值也被正确转发,用T&作f的参数类型是行不通的,唯一能够正确转发non-const右值的办法是用const T&来接受它,但前面又说过,用const T&行不通,因为const T&不能正确转发non-const左值。

Const& + non-const&
那两个加起来如何?

template<typename T>
void f(T& t)
{
g(t);
}

template<typename T>
void f(const T& t)
{
g(t);
}

一次重载。我们来分析一下。

对于non-const左值,重载决议会选中T&,因为绑定到non-const引用显然优于绑定到const引用(const T&)。

对于const左值,重载决议会选中const T&,因为显然这是个更specialized的版本。

对于non-const右值,T&根本就行不通,因此显然选中const T&。

对于const右值,选中const T&,原因同第二种情况。

可见,这种方案完全保留了参数的左右值和const/non-const属性。

值得注意的是,对于右值来说,由于右值只能绑定到const引用,所以虽然const T&并非“(non-)const右值”的实际类型,但由于C++03只能用const T&来表达对右值的引用,所以这种情况仍然是完美转发。

组合爆炸
你可能会觉得上面的这个方案(const& + non-const&)已经是完美解决方案了。没错,对于单参的函数来说,这的确是完美方案了。

但是如果要转发两个或两个以上的参数呢?

对于每个参数,都有const T&和T&这两种情况,为了能够正确转发所有组合,必须要2的N次方个重载

比如两个参数的:

template<typename T1, typename T2>
void f(T1& t1, T2& t2) { g(t1, t2); }

template<typename T1, typename T2>
void f(const T1& t1, T2& t2) { g(t1, t2); }

template<typename T1, typename T2>
void f(T1& t1, const T2& t2) { g(t1, t2); }

template<typename T1, typename T2>
void f(const T1& t1, const T2& t2) { g(t1, t2); }

(完美的)解决方案
理想情况下,我们想要:

template<typename T1, typename T2, … >
void f(/*what goes here?*/ t1, /**/ t2, … )
{
  g(t1, t2);
}

填空处应该填入一些东西,使得当t1对应的实参是non-const/const的左/右值时,t1的类型也得是non-const/const的左/右值。目前的C++03中,non-const/const属性已经能够被正确推导出来(通过模板参数推导),但左右值属性还不能。

明确地说,其实问题只有一个:

对于non-const右值来说,模板参数推导机制不能正确地根据其右值属性确定T&的类型(也就是说,T&会被编译器不知好歹地推导为左值引用)。

修改T&对non-const右值的推导规则是可行的,比如对这种情况:

template<typename T>
void f(T& t);

f(1);

规定T&推导为const int&。

但这显然会破坏既有代码。

很巧的是,右值引用能够拯救世界,右值引用的好处就是,它是一种新的引用类型,所以对于它的规则可以任意制定而不会损害既有代码,设想:

template<typename T >
void f(T&& t){ g(t); }

我们规定:

如果实参类型为右值,那么T&&就被推导为右值引用。
如果实参类型为左值,那么T&&就被推导为左值引用。

Bingo!问题解决!为什么?请允许我解释。

f(1); // T&& 被推导为 int&&,没问题,右值引用绑定到右值。
f(i); // T&& 被推导为 int&,没问题,通过左值引用完美转发左值。

等等,真没问题吗?对于f(1)的情况,t的类型虽然为int&&(右值引用),但那是否就意味着t本身是右值呢?既然t已经是具名(named)变量了,因此t就有被多次move(关于move语意参考上一篇文章)的危险,如:

void dangerous(C&& c)
{
C c1(c); // would move c to c1 should we allow treating c as a rvalue
c.f(); // disaster
}

在以上代码中,如果c这个具名变量被当成右值的话,就有可能先被move掉,然后又被悄无声息的非法使用(比如再move一次),编译器可不会提醒你。这个邪恶的漏洞是因为c是有名字的,因此可以被多次使用。

解决方案是把具名的右值引用作为左值看待。

但这就使我们刚才的如意算盘落空了,既然具名的右值引用是左值的话,那么f(1)就不能保持1的右值属性进行转发了,因为f的形参t的类型(T&&)虽然被推导为右值引用(int&&),但t却是一个左值表达式,也就是说f(1)把一个右值转发成了左值。

最终方案
通过严格修订对于T&&的模板参数推导原则,以上问题可以解决。

修订后的模板参数推导规则为:

如果实参是左值,那么T就被推导为U&(其中U为实参的类型),于是T&& = U& &&,而U& &&则退化为U&(理解为:左值引用的右值引用仍然是左值引用)。

如果实参是右值,那么T就被推导为U,于是T&& = U&&(右值引用)。

如此一来就可以这样解决问题:

template<typename T>
void f(T&& t)
{
  g(static_cast<T&&>(t));
}

想想看,如果实参为左值,那么T被推导为U&,T&&为U& &&,也就是U&,于是static_cast<T&&>也就是static_cast<U&>,转发为左值。

如果实参为右值,那么T被推导为U,T&&为U&&,static_cast<T&&>也就是static_cast<U&&>,不像t这个具名的右值引用被看作左值那样,static_cast<U&&>(t)这个表达式由于产生了一个新的无名(unnamed)值,因而是被看作右值的。于是右值被转发为了右值。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 11楼 发表于: 2011-08-15
《C++0x漫谈》系列之:多线程内存模型
多线程内存模型

动机
内存模型是C++09最重大的特性之一,之所以重大是因为多线程并发编程将成为下一个十年的主题之一,对此C++小胡子Herb Sutter早有精彩的论述。

为什么在C++里面要想顺畅地进行多线程编程需要对标准进行修订(而不仅仅是通过现有的多线程库如POSIX、boost.Thread即可)呢?对此Hans Boehm在他的著名的超级晦涩难懂的paper——《Threads Cannot be Implemented as a Library》——里面其实已经详尽地阐述了原因,但是,一,尽管这篇paper被到处cite,newsgroup上面关于到底能不能用volatile来实现线程安全性这类问题还是争议不断。这方面就连C++牛魔王Andrei Alexandrescu都犯过错误,可见有多难缠。二,这篇paper很难读,一般人就算头悬梁锥刺股一口气读上N遍,一转眼的工夫就又成丈二和尚了。Memory-model与多线程是一个非常棘手的领域。记得Andrei Alexandrescu曾在一篇专栏文章里面提到,大意是说,泛型编程难、编写异常安全的代码更难,但跟多线程编程比起来,它们就都成了娃娃吃奶。

因此,要想比较透彻理解C++09内存模型的动机,光是Hans的那篇paper是不够的,C++09的内存模型沿袭的是Java的内存模型,Java社群在这个上面花了玩命的工夫,最后修订出来的标准的复杂度达到了不是给人看的地步(当然,只是其中的一个小部分,并非全部),Jeremy Manson在google做了一个关于java memory model的talk,也只是浅浅的从宏观层面谈了一下。所以既然Java社群已经花了这个工夫,而且C/C++/Java本就是同根生,所以也就乐得发扬一下拿来主义了,订阅了相关mailing-list的老大们会发现Java社群这方面的几个老大也时常在里面发言,语言无疆界啊:)

用一句话来说,修订C++的内存模型的原因在于:

现有的内存模型无法保证我们写出可移植的多线程程序

那为什么无法保证呢。对此许多人都用一句模棱两可令人摸不着头脑的话来解释:因为C++98中的内存模型是单线程的(虽然标准没有明确指出,但这是一个隐含结论)。这句话说了等于没说,让我想起那个关于数学家的笑话。人们难免要问,那为什么内存模型是单线程的就意味着无法写出可移植的多线程程序来呢?POSIX线程模型指导下不是存在了那么多的C/C++多线程程序吗?这又怎么解释呢?

所以,必得有一个最本质的解释,下面这个就是:

现有的单线程内存模型没有对编译器做足够的限制,从而许多我们看上去应该是安全的多线程程序,由于编译器不知道(并且根据现行标准(C++03)的单线程模型,编译器也无需关心)多线程的存在,从而可能做出不违反标准,但能够破坏程序正确性的优化(这类优化一旦导致错误便极难调试,基本属于非查看生成的汇编代码不可的那种)。

OK,说到现在,对什么是“内存模型”还没有解释呢。内存模型的正式定义很是飘逸,jsr133中这么说:

A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program.
内存模型描述给定程序的某个特定的执行轨迹是否是该程序的一个合法执行。

其实这句话正常人多读几遍多少还能有有点似乎理解的感觉。接下来一句话就更诡异了:

For the Java programming language, the memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.
对于Java来说,内存模型的工作模式如下:对一个给定执行轨迹上的每一读取操作(read),检查该读取操作所读到的对应的写操作结果(write)是否不违背一定的规则。

以上是内存模型的技术定义,其最大的缺点就是不能帮我们感性而直观的理解什么是内存模型。

目前的多线程编程模型从广义上来说一般不外乎共享内存模型(多个线程访问共享空间,通过加锁解锁和对全局变量的操作来进行交互)和消息传递模型这两种。其中共享内存模型目前仍然是主流中的主流(比如C家族语言用的就都是这一招),消息传递模型大家也都不陌生,目前最成熟的应用是在Erlang里面。当然,这样的分类是往大了说,往小了说可就麻烦了,可以参考这里。

本文要说的内存模型是针对共享内存下的多线程并发编程的。内存模型的技术定义刚才已经饶舌过了,其非技术定义是这样的:

一个内存模型对于语言的实现方回答这样一个问题:
哪些优化是被允许的(这里的“优化”其实僵硬地说应该是transformations,当然,由于这是我杜撰的非官方定义,所以就管不了那么多繁文缛节了。)

另一方面,一个内存模型对于语言的使用方回答这样一个问题:
需要遵循哪些规则,才能使程序是正确的。(废话,这里的“正确”当然是多线程意义下的正确了。)

了解了这个定义之后我们便可以对号入座来拷问目前的C++03标准为什么在多线程上出了问题了。

为什么C++03标准不能保证多线程正确性
我们来看一个简单的例子:

int count = 0;
bool flag = false;

Thread1??????Thread2
count = 1;????while(!flag);
flag = true;?? r0 = count;

按照我们的直觉,r0不可能为读到count为0,因为等到while(!flag)执行完毕的时候,flag必定已经被赋为true,也就是说count=1必定已经发生了。

然而,实际上,在C++03下,r0读到count为0的可能性是存在的,因为count=1和flag=true的次序可以被颠倒。为什么可以被颠倒,是因为颠倒不影响所谓的Observable Behavior。

Observable Behavior: 标准把Observable Behavior(可观察行为)定义为volatile变量的读写和I/O操作。原因也很简单,因为它们是Observable的。volatile变量可能对应于memory mapped I/O,所有I/O操作在外界都有可观察的效应,而所有内存内的操作都是不显山露水的,举个简单的例子:

int main()
{
int sum;

for(int i = 0; i < n ; ++i) sum += arr[ii];
printf(“%d”, sum);
}

如果编译器知道arr里面各项的值(如果arr事先被静态初始化了的话),那么那个for循环就可以完全优化掉,直接输出arr各项和即可。为什么这个for循环可以优化掉?因为它不具备Observable Behavior。

有点迷糊?Hans Boehm的paper上的例子更简单一点:

x = y = 0;
Thread1??Thread2
x = 1;??????y = 1;
r1 = y;???? r2 = x;

很显然,结果要么r1==1要么r2==1。不可能出现r1==r2==0的情况,因为如果r1读到y值是0,那么表明r1=y先于y=1发生,从而先于r2=x发生,又由于x=1先于r1=y发生,因而x=1先于r2=x发生,于是r2就会读到1。

当然,以上这段分析是理论上的。地球人都知道,理论上,理论跟实际是没有差别的,但实际上,理论跟实际的差别是相当大滴。对于本例来说,事实上r1==r2==0的情况是完全可能发生的。只需把Thread1里面的两个操作互换一下即可。为什么可以互换呢?因为这样做并不违反标准,C++03是单线程的,而互换这两个操作对单线程的语意完全没有任何影响(对于一根筋通到底的编译器来说,它们眼里看到的是x、y这两个无关的变量)。

为什么volatile是个废物
那么,你可能会问,volatile可不可以用在这里,从而得到想要的结果呢?很遗憾,答案是否定的。对volatile的这个误解从来就没有停止过,去comp.lang.c++.moderated新闻组上搜一搜就会发现了,我怀疑在C++所有的语言特性所引发的口水中那些由volatile引发的至少要占到30%。volatile当之无愧为C/C++里面最晦涩的语言特性之一。

为什么volatile不可以用在这里,Scott Meyers和Andrei Alexandrescu作了一个极其漂亮的阐述,ridiculous fish同学也写了一个漂亮的post。不过瘾的话这里还有一份由Java大牛们集体签名的申明。

总而言之,由于C++03标准是单线程的,因此volatile只能保证单线程内语意。对于前面的那个例子,将x和y设为volatile只能保证分别在Thread1和Thread2中的两个操作是按代码顺序执行的,但并不能保证在Thread2“眼里”的Thread1的两个操作是按代码顺序执行的。也就是说,只能保证两个操作的线程内次序,不能保证它们的线程间次序。

一句话,目前的volatile语意是无法保证多线程下的操作的正确性的。

为什么多线程库也(基本)是废物
那么,同样又会有人问了:那么库呢,可不可以通过多线程库来编写出正确的多线程程序呢?这就是Hans Boehm那个paper所要论述的内容了。该paper全长仅仅8页,核心内容也不过两三页纸。但由于涉及了对标准中最晦涩的内容如何进行解释,所以非常难读。其实它的中心思想可以用一句简单的话概括出来:

因为C++03标准是单线程的,所以即便是完全符合标准的编译器也可能各个脑袋里面只装着一个线程,于是在对代码作优化的时候总是一不小心就可能做出危害多线程正确性的优化来。

Hans Boehm在paper里面举了三个例子,每个例子都代表一类情况。

第一个例子:

x = y = 0;
Thread1???????? Thread2
if(x == 1) ++y;????if(y == 1) ++x;

以上代码中存在data-race吗?由于x和y一开始都是0,所以答案是:不存在,因为两个if条件都不会满足,从而对x和y的++操作根本就不会被执行。但,真正的问题是,在现行标准下,编译器完全可以作出如下的优化:

x = y = 0;
Thread1???????? Thread2
++y;???????????? ++x;
if(x != 1) --y;??????if(y != 1) --x;

于是data-race大摇大摆地出现了。你能说这是编译器的错吗?人家可是遵章守纪的好市民。以上的代码转换并没有违背任何单线程内的语意。所以,唯一的错误是在标准本身身上。标准只要说一句:在这种情况下,所有的sequential consistent的执行路径都不可能导致data-race,因此,该程序内不存在data-race。就万事大吉了。

第二个例子:

struct
{
int a : 17;
int b : 15;
} x;

一个线程写x.a,另一个线程写x.b。有data-race吗?目前的标准对此一言不发。我们最妥善的做法也只能是在无论读取x.a或是x.b的时候将整个x哐当用锁锁起来。

第二一撇个例子:

struct { char a; char b; char c; char d;
char e; char f; char g; char h; } x;

那么如下的操作

x.b = ’b’; x.c = ’c’; x.d = ’d’;
x.e = ’e’; x.f = ’f’; x.g = ’g’; x.h = ’h’;

会涉及到x.a吗?答案是会,因为编译器只要这么转换一下:

x = ’hgfedcb/0’ | x.a;

如果原先的代码中有另一个线程在对x.a进行写操作,data-race就不幸发生了。而且还是违反直觉的发生的——程序员泪眼汪汪的问:我操作x.b~x.h关x.a什么事呢?

问题就在于,现行标准没说清什么是一个“内存位置(memory location)”。于是N2171提案里面是这样写的:

A memory location is either an object of scalar type, or a maximal sequence of adjacent bit-fields all having non-zero width. Two threads of execution can update and access separate memory locations without interfering with each other.
一个内存位置要么是一个标量、要么是一组紧邻的具有非零长度的位域。两个不同的线程可以互不干扰地对不同的内存位置进行读写操作。

第三个例子:

for (...) {
...
if (mt) pthread_mutex_lock(...);
x = ... x ...
if (mt) pthread_mutex_unlock(...);
}

对于以上代码,貌似是不会有data-race了,因为x的访问已经被pthread_mutex_(un)lock()包围(保卫?)起来了。但果真如此吗?

“聪明”的编译器只要运用一种“成熟”的叫做register promotion的技术就可以破坏这段表面平静的代码:

r = x;
for (...) {
...
if (mt) {
x = r; pthread_mutex_lock(...); r = x;
}
r = ... r ...
if (mt) {
x = r; pthread_mutex_unlock(...); r = x;
}
}
x = r;

在单线程上下文中,以上优化是完全合法的,而且也的确能够带来效率提升。但由于它将原本只能位于临界区内部的x的写操作“提升”到了临界区外面。结果到了多线程环境下就挂了。对此POSIX线程库也无能为力。

那么,究竟如何才能允许用户编写正确的多线程代码呢?

一个简单的办法就是禁止编译器作任何优化:所有的操作严格按照代码顺序执行,所有的操作都触发cache coherence操作以确保它们的副作用在跨线程间的visibility顺序。但这样做显然是不实际的——多线程本来的目的就是为了提升效率,这下倒好,为何实现多线程正确性却要付出巨大的效率代价了。但为什么要考虑这个方案呢,目的就是要明确我们的目的是什么:我们的目的是,使代码能够“看起来像是”被“顺序一致性(sequential consistency)地”执行的。所谓顺序一致性其实没什么神秘的,我们一开始被教导的多线程程序被执行的方式就是所谓的顺序一致性的:即多个线程的所有操作被穿插交错执行,但各个线程内的各操作之间的相对顺序被遵守——别紧张,就是你脑袋里那个对于多线程如何被执行的概念。

那么,要想实现顺序一致性,难道除了禁止一切优化就没有其它办法了吗?我们注意到,实际上在一个线程内部,几乎绝大多数的操作都是单线程的,也就是说,它们操作的都是局部变量,或者进一步说,对其它线程不可见的变量。也就是说,绝大多数的操作对其它线程来说都是不可见的。对于这部分操作,编译器完全可以自由地按照单线程语意来进行优化,动用所有古老的单线程环境下的优化技术都没问题。最关键的就是那部分“线程间可见”的操作。对于这部分操作,编译器必须确保它们的“对外形象”。

那么,编译器能否分辨出哪些操作是单线程的,哪些操作是多线程的呢?很大程度上,这是可以的。所有对局部变量的操作,都是单线程可见的。所有对全局变量的操作都是多线程可见的。因此是不是可以说,编译器只要对那些全局变量操作小心一点就可以了呢?答案是还不够。因为这样的编译模型要求编译器对所有针对全局变量的操作都禁用任何优化,并且还要时不时通过插入memory fences(或称memory barrier)来确保cache coherence。这个代价,还是太大。

于是所谓的data-race-free模型粉墨登场。Data-race-free模型的核心内容是:

只要你通过基本的同步原语(由标准库提供,如Lock)来保证你的程序是没有data-race的,那么编译器就能向你保证你的程序是被sequentially-consistent地执行的。

为什么这个模型是有优势的。是因为它最大化了编译器可能作的优化。举个简单的例子:

… // #1
Lock(m)
… // #2
Unlock(m)
… // #3

在这样的一段程序中,#1、#2、#3处的代码完全可以享受所有的单线程优化。编译器再也不用去猜测哪些操作是有线程间语意的哪些操作没有了,省心省事。在以上的程序中,Lock(m)和Unlock(m)就充当了所谓的one-way barrier(单向内存栅栏),不同的是Lock(m)具有Acquire语意,而Unlock(m)具有Release语意。Acquire语意是说所有下方的操作都不能往其上方移动,Release语意则相反。对于上面的代码来说也就是说,编译器的优化不能将#2处的代码移到临界区外,但可以将#1、#3处的代码往临界区内移。

至于为什么data-race-free能够确保sequential-consistency,我以前写过一篇文章阐述这一点,其实也就是阐述这个经典的证明。不过由于是用英文写的,所以读的人很少。因为当时没有作任何的铺垫,所以读懂的就更少了。

延伸阅读
这方面的延伸阅读太多太多,并发编程历史悠久,其间的paper不计其数。这里只推荐一些重要且基础的:

Shared Memory Consistency Models: A Tutorial对共享内存一致性模型作了一个非常漂亮的介绍。

The Java Memory Model对Java1.5的内存模型作了详细的阐述,由于C++的内存模型基本是沿用Java的,因此弄清这篇paper讲的东西对理解C++内存模型有非常大的意义。只不过Java为了考虑安全性,使得其内存模型的某些部分极其复杂,所以我建议这篇paper只读前1/2就差不多了。一开始的部分,对修订内存模型的动机阐述得非常透彻。

The Performance of Spin Lock Alternatives for Shared-Money Multiprocessors是篇非常有趣的paper,对一个简单的spin lock的各种方案的性能细节作了详细的分析,尤其是深入cache coherence如何影响性能的那些地方,阐述得非常到位。

Sequencing and the concurrency memory model (revised)(a.k.a 提案N2171)是C++09的内存模型提案,目前已经相当完善。

Memory Model Rationales介绍了C++09内存模型的理念,知其然知其所以然,值得一读。

此外,Hans Boehm的主页上列了一大堆相关的资料,都很有用。

最后来一个号外,C++大胡子二号Lawrence Crowl最近又在google做了一场关于C++ Threads的talk:-)
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 12楼 发表于: 2011-08-15
C++ 0x 里垃圾收集器的讲座
好像最近C++标准地下工会在公司附近开会,所以我们上班时偶尔可以看到工会成员矫健的身影。我们也近水楼台,得以听到关于C++0x进展的一些科普报告。上次Bjarne Stroustrup做了关于泛型编程的讲座后,Symantec实验室的Michael Spertus也做了一个关于C++ 0x里垃圾收集的讲座。Michael Spertus当年写出了IBM PC上最早的C编译器,也是C++ 0x里垃圾收集器的主要倡议人。

Michael从什么是GC开始讲起。三言两语后,谈到在C++ 0x里加入GC的动机。一是许多数据结构,对象,或者资源的生命期难以事先确定、静态管理。我们需要某种形式的动态管理技术。很多老大以为C++高手的标志之一是不需要GC。如果这样想,Unreal的Tim Sweeney就笑了。Unreal引擎里就大量使用GC。如果一个系统需要管理大量对象,要求高吞吐量,但可以容忍偶尔的系统延迟的话,GC是颇为不错的工具。所以3D/建模,2D图像处理等计算都可以用到GC。第二个理由非常有煽动力:帮助C++程序清除内存泄露。我们不必在C++里排斥人肉内存管理。问题是,人肉内存管理难以尽善尽美。大型系统里内存泄露几乎不可避免。每次泄露也不多,10来KB到几个MB。但积累起来,也就造成诸多问题。因为每次泄露量不大,也不像Java等基于GC的语言,“泄露”随时都在发生。所以可以通过定期执行GC来清除这些泄露,也不用消耗过多资源。Michael后来举了一个颇有说服力的例子:某电信公司的交换机,100多万行程序,有持续的内存泄露。每小时必须重启一次。使用GC后,内存泄露消失,交换机不用再定时重启。重启时系统堆上有大概200个线程,500MB内存。这样算来,每线程每小时泄露2.5M。每线程每分钟不过42KB。而每分钟收集42KB内存对系统根本不会有什么影响。最后实测下来,收集500MB不过需要两秒种。分摊到一个小时内,完全可以忽略不计。这样的GC应该叫LC—Litter Collector。还有一个例子是Michael向Mozilla浏览器里注入GC。每次用户操作使得GC回收大概10KB的内存,实在是小菜一碟。

Michael接着谈到C++ GC的发展。C++ GC已经成熟,可以标准化了。系统研究的老大们已经做了20年的研究。GC也用到了形形色色的C++系统里。标准化的关键是C++的GC是可选项:GC可以被关掉,而且程序员能够在代码里随时决定用GC还是人肉内存管理。这样才能满足某些系统实时回收资源的要求。而且已有的标准库就不需要重新编译。

Michael比较了shared-ptr和GC。GC的主要缺陷是不能即时回收内存,而强项除了方便程序员外,就是性能了。GC的性能(尤其是分摊后的性能)在多方面的基准测试里超过自动指针管理已经不是新闻。Michael举了Boost线程安全基准测试的数据。GC比用share-ptr的测试程序快10倍以上。比Java的hotspot程序或者Gcj编译出的程序快5倍以上。甚至用人肉内存管理的C程序也比GC慢,不过比Boost的程序员快3倍左右。

既然GC在C++里用了很多年了,干嘛还要标准化呢?主要有三条原因。一是内置的GC才能读取C++的类型系统。高质量的GC需要知道数据的类型信息。没有类型信息,GC变得非常保守,需要经常扫描大片内存来决定是否能回收某个对象。这样让GC变得很慢。第二个原因是我们需要防止编译优化对GC造成破坏。如果不标准化,标准的C++编译器不可能知道第三方的GC需要什么样的优化。第三是为了允许不同的厂家能共享GC管理的对象。不然微软在某个控件里用了GC,怎么能保证同样的控件到了Borland的系统里也正常回收内存呢?

目前的C++ 0x的提议走的是稳健路线。任何功能都要经过实际应用的检验。GC用于特殊场合(比如前面的LC)。通过句法糖的方法实现,也就是说没有新的关键词。API的数目要尽可能小。当然了,C++的类型系统不如Java那么强,所以GC也相应复杂一些。标准提案里包涵了好几种GC类型:
gc_required: 所有资源都要通过GC收集。由gc_required控制的区域内所有指针都是经过标注的gc指针。
gc_forbidden: 不允许GC。必须人肉回收内存。由gc_forbidden控制的代码区域里只有原始指针。
gc_safe:缺省情况。忽略人肉管理的内存,但处理没有被手工回收的部分。
gc_strict: GC处理的代码段里整型变量不包含指针(比如不能在DWORD里放指针)。
gc_relax: GC处理的代码段里整型变量可以包含指针。这种情况下GC往往需要扫描大量内存来确保安全回收内存。
gc_cast: 把一种类型的GC转换成另外一种类型。

下面是个例子:
class A {
            A *next;
            B b; // 这里不能用gc_strict, 因为我们不知道B里的数据信息。
            gc_stric int data[10000]; // 我们肯定data数组里没有指针
}

同时C++标准库里还提供两个函数:bool std::is_garbage_collected(),和class std::gc_lock()。一个用来检查某块内存是否被GC回收,另外一块用来设定所谓的GC临界区。临界区内的内存不能被回收,这样可以防止GC不合时宜地启动。

呵呵,够复杂吧?引入这么多操作的原因之一是要和老的C++系统互动。比如说下面一些情况:
两个DLL,一个用了全局GC,另外一个没有怎么办?如果每个DLL都链接到自己的C类库怎么办?
如果一个共享库希望在内部使用GC,但不影响堆的其它部分怎么办?
如果我们在C++程序内调用libjvm怎么办?libjvm装载后,C++程序可以读取到载入的JVM里的堆,未必我们需要对已经有Java GC控制的内存再做垃圾处理?

针对不同的情况,我们得祭出不同的GC。嘿嘿,C++程序员们,准备好你们的钱包哈。如果2009年后C++ 0x开始流行,要买的书就多了。比如Effective C++ GC, Master C++ GC, C++ GC with Legacy Applications,  Modern C++ Design with GC, Expert C++ with GC, Expert C++ without GC。这个世道,不容易啊。


那finalization嗫?没有了finalization的GC还叫GC么?C++的大仙们当然不会漏掉这么重要的环节。只不过因为finalization本身就很复杂了(比如说回收内存时应该怎样调用回收对象的析构函数?如果回收时立刻调用,会造成线程不安全),所以有另外的提案讨论乐。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 13楼 发表于: 2011-08-15
C++0x内存模型和序列点的一些思考

这几天断断续续一直在看关于C++0x的并发内存模型的proposal,这堆proposal显然是所有0x proposals里面最困难的。看的时候问题不断,单单是对原始单线程内存模型的改进和序列点概念的精化就非常tricky。所以看的时候总结了一些模糊的地方,昨天给发到comp.lang.c++.moderated上面去了,Francis Glassborow给了一个回贴澄清了里面的一个问题,不过尚留下不少其它的。相信后面还会有热心人(james kanze, maybe:))回复的,呵呵。
贴在这里,欢迎有兴趣的朋友一起探讨:-)
没时间细细转成中文的了,就直接把写的帖子贴在下面了,呵呵:)
[后记] 果然不出我所料,新闻组上的大牛人&大热心人James Kanze果然火速给出了最为详尽的回复,哈哈,我帖子里的绝大部分推测都是对的:) 有兴趣的朋友可以查看新闻组上的链接。





1. When the standard says that the order of evaluation of arguments to a function is unspecified, does that mean they could be overlapped, or just indeterminately ordered?
This problem occurs to me when I was reading Boehm's memory model proposal [N2052], in which he suggested using "sequenced before" instead of "sequence point" and then stipulated the evaluation order of function arguments as "unsequenced", which explicitly allows overlapping according to his proposal.
So, my question is, are those two constraints (i.e. "unsequenced evaluation order" and "unspecified evaluation order") essentially equal? 'cause I was thinking that "unsequenced" is looser than "unspecified evaluation order".
I couldn't find any statement about whether or not an unspecified order allows overlapping in C++98. So, Could any one give an explanation please?

P.S. According to another statement of the evaluation order, which says "there's an sequence point after the evaluation of all the arguments and before the entry of the function", this obviously means that the value computation and side effect of the evaluations of the arguments could take place in any order, even overlapped (?), because there's no sequence point between them, right? So the implementation is free to rearrange the instructions.
Plus, according to my understanding of the sequence point, it doesn't really matter whether or not the evaluations of several sub-expressions between two adjacent sequence points (may we now say "not sequently related"?) could be overlapped, because it would be just a specification of the semantics of the abstract machine anyway. So even if the standard says that they should not be overlapped, a particular implementation can make them overlapped anyway, because "If a side effect on a scalar object is not sequenced relative to either a different side effect on the same scalar object, or a value computation using the value of the same scalar object, the behavior is undefined. [Excerpt from N2052]" which guarantees that overlapped evaluation of the arguments doesn't really change anything.
The sentence quoted above, according to my understanding, is very important, it essentially says that we can view all the value computations between two adjacent sequence points as operating on immutable variables because otherwise the program will have undefined behavior (e.g. a store operation on some variable used by a value computation would break the rule). And now that they operate on immutable variables, we can execute them disorderly, unsequencedly, overlappingly, or whatever, right?

2. Another question of which I want to make sure my judgment is right; the following call has undefined behavior, right?
f(i,i++); // undefined behavior.

3. There's a somewhat confusing example in N2052, see below:
x++ + increment_x(); // Evaluation order unspecified; x may be incremented only once

How could x be incremented only once when there’re obviously two "++"s. The only way that can happen is one of the two "++"s is thrown away.
I guess what the example really wants to say is that "the value of x may be x+1 instead of x+2", since the compiler could rearrange the execution order like this:

#assuming x originally is 1
load x into some register say r1 ( here we have r1==1)  // the first 1/3 of evaluating x++, the left operand of operator +
increment_x() // this makes the value of x 2, here we have completed the evaluation of the right operand of operator +
store r1+1 to the memory location of x  // this makes the value of x r1+1, which is 2! not 3.
return r1 as the value computation of the left operand of "+" // we got 1
as a last step, the evaluation of "+" is done here, which gives a value of 2, which perhaps lots of people assume not.

While we are done, we can see that the value of x is 2, not 3!
So, while the exact words, I think, should be "x is incremented twice but the final value of x is the value of original x plus 1 so that it's SEEMINGLY incremented once", of course we could say x is incremented just once, because the result equals x+1, but that would be confusing and misleading, at least at first to me (coz I was all "how in the name of god is that going to happen?":-) ). So I suggest clarifying this example by giving more detailed explanation, after all it's a pretty important example, right?
And since we are on that, there's a tiny typo in the definition of increment_x():

int increment_x() { x++; } // here we dropped a "return".

Plus, should "x++ + increment_x();" have undefined behavior according to the rule: "If a side effect on a scalar object is not sequenced relative to either a different side effect on the same scalar object, or a value computation using the value of the same scalar object, the behavior is undefined"?

4. If C++98 means overlap-able when it says unspecified evaluation order, then why Boehm, in N1944, says below:

[Excerpt from N1944]
Not every side effect or evaluation can be authoritatively determined to be either previous to or subsequent to a given sequence point. For example, given

a = 0;
b = (a = 1, 2*a) + 3*a;

The evaluation of “3*a” is not ordered with respect to the sequence point introduced by the comma operator. It may be previous or subsequent (NOTE HERE!!); the standard simply doesn't say. Therefore, that evaluation may or may not be separated from the assignment by a sequence point.
[End]

Why is it "It may be previous or subsequent"? If unspecified evaluation means "could overlap", then this should really be "It may be previous or subsequent, or overlapped with the sequence point introduced by the comma op." When I say "overlapped with", I mean like:

load a into some register say r1
(a=1, 2*a)
plus r1 by 3
...

That means the evaluation of "3*a" is _splitted_ by the sequence point introduced by the comma op.
Is this ordering allowed by the current standard (C++98)? If it is, then would it conflict with 1.9p7:
....called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place?

5. from clause 5 paragraph 4:
"Between the previous and next sequence point a scalar object shall ... ... .The requirements of this paragraph shall be met for each allowable ordering of the sub-expressions of a full expression; otherwise the behavior is undefined."

Here, what are these "allowable ordering of the sub-expressions"? According to my recollection, in no place has the current standard clearly stated which ordering is allowable and which is not. "Unspecified" is not clear enough to me, especially when it comes to whether or not it allows overlapping, which kind of is the core of all the questions I brought up here.

6. If unsequenced evaluation(overlapped execution, in particular) is allowed as said in N2052, would the evaluation of some simple expression give unpredictable value, which in turn makes the behavior undefined? Here's an example:

double d = ... ;
double g(double& d){ return d*=3.12; }

f( ++d, g(d) ); // should this be undefined behavior?

Now let's just say that we're on such an architecture that storing a double consists of storing its lower and higher DWORDs( if sizeof(double) equals that of a qword ) __separately__ .  (coz it seems I have qword-level store instruction on my x86 pc)
So, on such an architecture, the instructions generated for the call to 'f' above would possibly be like this:

load d and compute d+1
store the lower dword of the result into the lower dword memory location of d, which now makes d half-baked.
evaluate g(d), which will use the half-baked d to compute d*3.12, and store that into d, making d into a unreasonable/ridiculous state
store the higher dword of the result computed beforehand by "++d" into the high dword memory location of d, which makes the value of d even more ridiculous.
call f

So, it seems this clearly should be categorized as undefined behavior, isn't it? But the real question is, which rule in the standard state clearly that this is undefined behavior, or instead, which rule in N2052 did? Plus, if the behavior of this example is undefined, then the "x++ + increment_x(); " example in N2052 would have undefined behavior, too, right?

7. excerpt from N2052:
"Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function."

I think this is overconstrained, because it will make "++d" indeterminately sequenced with respect to "g(d)" in "f( ++d, g(d) );"(see the example above), while actually they're supposed to be unsequenced. (if "f( ++d, g(d) );" is not clear enough, how about "++d + g(d);" ?)
But I'm not quite sure about this, so correct me if I made any stupid mistake.


I need clear answers to these questions, every single one, please, because the memory model is really driving me crazy these days :-)
Again, any help would be greatly appreciated, thanks you guys!
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 14楼 发表于: 2011-08-16
C++2011添加的关键字

关键字:
constexpr
auto
decltype
static_assert
char16_t
char32_t
wchar_t
nullptr
符号:
->
[]
>>
&& "rvalue reference"
[[ 和 ]]
概念:
模板别名 template alias
后缀语法 suffix syntax
右值引用 rvalue reference
初始化列表
默认函数/删除函数 defaulted functions and deleted functions
静态断言 static assertion
匿名函数 lambda function
强化枚举/枚举类 enum classes
变参模板 variadic templates
强化联合/通用联合 Union (generalized) 允许非简单类型成员
常量表达式 Generalized constant expressions
乾坤大挪移 move sematic
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 15楼 发表于: 2011-08-19
C++0x中的多线程例子
C++ 0x 标准将增加对多线程的支持。以后所有的编译器都必须遵循新标准中对多线程的规定,这将会给不同平台上程序的移植带来很大的方便。
让我们先来看看std::thread类,它负责管理线程的执行过程。
启动线程
我们创建一个std::thread类的实例来启动一个新线程,用一个线程函数作为构造函数的参数。如
     void do_work();
     std::thread t(do_work);
std::thread类也接受一个函数对象作为参数。
     class do_work
     {
     public:
         void operator()();
     };
     do_work dw;
     std::thread t(dw);
注意这里只是传了一个对象的拷贝进去。如果想传递对象本身(你应该确保在线程结束前它没有被销毁),可以使用std::ref。
     do_work dw;
     std::thread t(std::ref(dw));
很多创建线程的API允许传递一个参数给线程,例如long或void*。Std::thread也支持参数,并且可以是任意类型任意个数的参数。
     void do_more_work(int i,std::string s,std::vector<double> v);
     std::thread
         t(do_more_work,42,"hello",std::vector<double>(23,3.141));


也可以用std::ref传引用。


     void foo(std::string&);
     std::string s;
     std::thread t(foo,std::ref(s));
知道了怎么启动线程,那我们如何等待线程结束呢?新标准中有个术语叫joining来表示与线程协同工作, 我们使用成员函数join()


    void do_work();
    std::thread t(do_work);
    t.join();
如果你不想joining你的线程,可以销毁线程对象或者调用detach()。
     void do_work();
     std::thread t(do_work);
     t.detach();


启动线程就这么简单。接下来我会介绍一下线程间如何共享数据。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 16楼 发表于: 2011-08-19
C++0x中的多线程例子
同许多线程API一样,C++0x用互斥来保护共享数据。有四种互斥类型:







Non-recursive (std::mutex)
Recursive (std::recursive_mutex)
允许锁超时的non-recursive  (std::timed_mutex)
允许锁超时的recursive (std::recursive_timed_mutex)





如果你试图在一个线程上锁(lock)一个non-recursive mutex两次而当中没有unlock它的话,会产生未知结果。递归recur6sive mutex只是增加锁的计数,因此必须确保你unlock和lock的次数相同,其他线程才可能锁这个mutex。





通常我们用模板类std::unique_lock<>和std::lock_guard<>来lock和unlock一个mutex。这些类在构造函数中lock一个mutex,在析构函数中unlock它。因此,如果你用的是局部变量,你的mutex会在退出作用域时自动被unlock。
    std::mutex m;
    my_class data;
    void foo()
    {
        std::lock_guard<std::mutex> lk(m);
        process(data);
}   // mutex unlocked here

std::lock_guard只能像上面这样使用。而std::unique_lock允许延迟lock、设置超时,以及在对象销毁之前unlock。
如果你选择std::timed_mutex来设置锁超时的话,那需要使用std::unique_lock:
    
    std::timed_mutex m;
    my_class data;
    void foo()
    {
        std::unique_lock<std::timed_mutex>
            lk(m,std::chrono::milliseconds(3)); // wait up to 3ms
        if(lk) // if we got the lock, access the data
            process(data);
}   // mutex unlocked here

由于这些lock类是模板,因此他们可以用于所有标准的mutex类型,以及提供了lock()和unlock()函数的扩展类型。

避免死锁

有时候,我们需要锁多个mutex。如果控制不力,可能导致死锁(deadlock):两个线程都试图锁相同的mutex,每个线程都锁住
一个mutex,而等待另外一个线程释放其他的mutex。C++0x考虑到了这个问题,你可以使用std::lock函数来一次锁住多个mutex,
而不必冒着死锁的危险来一个一个地锁:

    struct X
    {
        std::mutex m;
        int a;
        std::string b;
    };

    void foo(X& a,X& b)
    {
        std::unique_lock<std::mutex> lock_a(a.m,std::defer_lock);
        std::unique_lock<std::mutex> lock_b(b.m,std::defer_lock);
        std::lock(lock_a,lock_b);

        // do something with the internals of a and b
    }

在上面的例子中,如果你不使用std::lock的话,将很可能导致死锁(如一个线程执行foo(x,y), 另一个执行foo(y,x))。
加上std::lock后,则是安全的。


在初始化时保护数据

    如果你的数据需要在初始化时被保护,就不能再使用mutex了。因为在初始化结束后,这会引起不必要的同步。C++0x提供了很多方法来在初始化时保护数据。

1)假定你的构造函数是用constexpr关键字声明并且满足常量初始化的条件。在这种情况下,一个静态存储区的对象在静态初始阶段会确保在其他代码运行之前被初始化。对于std::mutex来说,这是最佳选择,因为它消除了全局mutex初始化时产生紊乱的可能性。

             class my_class
             {
               int i;
    
               public:
               constexpr my_class():i(0){}
    
               my_class(int i_):i(i_){}
    
               void do_stuff();
             };
    
             my_class x; // static initialization with constexpr constructor
    
             int foo();
             my_class y(42+foo()); // dynamic initialization
    
             void f()
            {
              y.do_stuff(); // is y initialized?
            }

2)在一个块作用域(block scope)中使用静态变量。在C++0x中,块作用域的静态变量在函数第一次被调用时初始化。如果另一个线程在初始化完成之前试图调用该函数,它必须等待。

             void bar()
             {
               static my_class z(42+foo()); // initialization is thread-safe

               z.do_stuff();
             }

3)如果以上情况都不适用(对象可能是动态创建),那么最好使用std::call_once和std::once_flag。从名字就可以看出,std::call_once用于与一个std::once_flag实例协作,指定的函数将只会执行一次。

             my_class* p=0;
             std::once_flag p_flag;
      
             void create_instance()
             {
               p=new my_class(42+foo());
             }
      
             void baz()
            {
               std::call_once(p_flag,create_instance);
               p->do_stuff();
            }

    同std::thread构造函数一样,std::call_once也可以接受函数对象作为参数,并且接受多个参数。再次强调,默认是传拷贝。如果要传引用,请使用std::ref.

等待事件

        如果想在线程间共享数据,通常需要一个线程等待另一个线程执行某些操作。我们希望这不要花费CPU时间。如果线程只是等待访问共享数据,那mutex锁就足够了。不过,这样做有时并达不到想要的结果。

       最简单的方法是让线程Sleep一段时间,然后去检查是否可以进行想要的操作。一定要确保你用来保护指示事件已经发生的数据的mutex在线程休眠的时候已经unlock(这话是不是听着很别扭?呵呵,我也不知道怎么翻译会容易让人理解一些。不过看了下面这段代码,相信你会明白的):

             std::mutex m;
             bool data_ready;
      
             void process_data();
      
             void foo()
             {
                std::unique_lock<std::mutex> lk(m);
                while(!data_ready)
                {
                  lk.unlock();
                  std::this_thread::sleep_for(std::chrono::milliseconds(10));
                  lk.lock();
                }
                process_data();
            }

      这是最简单的方法,但是并不好。有两个原因。第一,在数据准备好之后,线程在被唤醒后去检查数据之前平均需要等待5毫秒。这有时会造成明显的滞后。尽管可以通过减少等待时间来解决,但是这会带来第二个问题:每隔10毫秒,线程必须醒来,获得mutex,检查flag,即使什么也没有发生。这将耗费CPU时间和增加对mutex的抢占。因此,这将潜在地减慢了正在等待的线程去执行任务。

      如果你发现你的代码是那么写的,那么请用条件变量来代替。不要让线程等待一段确定的时间,你可以让线程休眠直到收到另一个线程通知。这可以有效地让等待线程的CPU使用率为0。我们可以用条件变量来重写foo函数:  
            std::mutex m;
            std::condition_variable cond;
        
        
            bool data_ready;
        
            void process_data();
        
            void foo()
            {
                std::unique_lock<std::mutex> lk(m);
                while(!data_ready)
                {
                    cond.wait(lk);
                }
                process_data();
            }

        注意上面的代码把 lock对象lk作为参数传给了wait()函数。条件变量在wait()函数的unlock这个mutex,在退出函数的时候再将其lock。这样可以确保线程在休眠时被保护的数据可以被其他线程修改。设置data_ready标志的代码可以这样写:  
            void set_data_ready()
            {
                std::lock_guard<std::mutex> lk(m);
                data_ready=true;
                cond.notify_one();
            }
       你仍然需要检查数据是否已经准备好,因为条件变量有可能被恶意唤醒,此时wait()函数将返回尽管它没有被另一个线程通知。如果你担心这种情况,你可以让标准库来帮你搞定。你只需要指明你在等待什么即可。  
            void foo()
            {
                std::unique_lock<std::mutex> lk(m);
                cond.wait(lk,[]{return data_ready;});
                process_data();
            }

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 17楼 发表于: 2011-10-12


国际标准组织ISO/IEC发布了C++11编程语言标准,售价352瑞士法郎。
C++作者Bjarne Stroustrup在自己网站上提供了标准草案文本(PDF),应该与最后的标准内容相差不大。
C++0x/11标准在今年8月获得一致通过,它是1998年以来C++语言的第一次大修订,对C++语言进行了改进和扩充,新的特性也扩展了语言在灵活性和效率上的传统长处,例如转移语义,lambda函数,等等。

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 18楼 发表于: 2011-11-18
现代C++风格的新元素
“C++11就像一门新的语言。” – Bjarne Stroustrup


C++11标准推出了很多有用的新特性。本文特别关注相比C++98更像是一门新语言的那部分特性,因为:


这些特性改变了编写C++程序使用的代码风格和习语[TODO],通常也包括你设计C++函数库的方式。例如,你会看到更多参数和返回值类型为智能指针(smart pointer),同时也会看到函数通过值传递返回大型对象


你将会发现在大多数的代码示例中充斥着新特性的身影。例如,几乎每5行现代C++代码示例都会使用到auto


C++11的其他特性也很棒。但是请先熟悉下面这些,正是因为这些特性的广泛使用使得C++11代码如同其他现代主流语言一样整洁、安全和高效,与此同时保持了C++传统的性能优势。


提示:


与Strunk & White[TODO]一样,本文只做概要的总结性指导而不做详尽基本原理和优缺点分析。详细分析请参见其他文章
本文会不断更新,主要变更及内容增加请参见文末变更记录
auto


基于以下两个原因,尽可能使用auto:首先,使用auto会避免重复声明编译器已经知道的类型。


// C++98
  map<int,string>::iterator i = m.begin ();


  // C++11


  auto i = begin (m);


其次,当使用未知类型或者类型名称不易理解时使auto会更加便利,例如大多数的lambda函数[TODO]——你甚至不能简单的拼写出类型的名字。


// C++98
  binder2nd< greater<int> > x = bind2nd ( greater<int>(), 42 );


  // C++11


  auto x = [](int i) { return i > 42; };


需要注意,使用auto并不改变代码的含义。代码仍然是静态类型[译注],每个表达式的类型都是清晰和明确的;C++11只是不需要我们重复声明类型的名字。一些人刚开始可能会害怕在这里使用auto,因为感觉好像没有(重复)声明我们需要的类型就意味着会碰巧得到一个不同的类型。如果你想要明确地进行一次强制类型转换,没有问题,声明目标类型就好了。然而大多数情况下,只要使用auto就可以了;几乎不会出现错误地拿到一个不同类型的情况,即便出现错误,C++的强静态类型系统也会由编译器让你知道这个错误,因为你正试图访问一个变量没有的成员函数或是错误地调用了该函数。


译注:动态类型语言(dynamic typing language)是指类型检查发生在运行期间(run-time)的语言。静态类型语言(static typing language)是类型检查发生在编译期间(compile-time)的语言。


智能指针:无须delete


请始终使用标准智能指针以及非占有原始指针(non-owning raw pointer)。绝不要使用占有原生指针(owning raw pointer)和delete操作,除非在实现你自己的底层数据结构这种少见的情况下(即使在此时也需要在 class 范围内保持完好的封装)。如果只能够知道你是另一个对象唯一的所有者,请使用unique_ptr来表示唯一所有权(TODO)。一个”new T”表达式会马上初始化另一个引用它的对象,通常是一个unique_ptr。


// C++11 Pimpl Idiom
  class widget {


  widget ();


  ~widget ();


  private:


  class impl;


  unique_ptr<impl> pimpl;


  };


  // in .cpp file


  class impl {


  :::


  };


  widget::widget ()


  : pimpl ( new impl () )


  {


  }


  widget::~widget () = default;


使用shared_ptr来表示共享所有权。推荐使用make_shared来有效地创建共享对象。


// C++98
  widget* pw = new widget ();


  :::


  delete pw;


  // C++11


  auto pw = make_shared<widget>();


使用 weak_ptr 来退出循环并且表示可选性(例如,实现一个对象缓存)


  // C++11
  class gadget;


  class widget {


  private:


  shared_ptr<gadget> g; // if shared ownership


  };


  class gadget {


  private:


  weak_ptr<widget> w;


  };


如果你知道另一个对象存在时间会更长久并且希望跟踪它,使用一个(非占有non-owning)原始指针。


// C++11
  class node {


  vector< unique_ptr<node> > children;


  node* parent;


  public:


  :::


  };


nullptr


始终使用nullptr表示一个null指针值,绝不要使用数字0或者NULL宏,因为它们也可以代表一个整数或者指针从而产生歧义。


Range for


基于范围的循环使得按顺序访问其中的每个元素变得非常方便。


  // C++98
  for( vector<double>::iterator i = v.begin (); i != v.end (); ++i ) {


  total += *i;


  }


  // C++11


  for( auto d : v ) {


  total += d;


  }


非成员(nonmember) begin 和 end


始终使用非成员begin和end,因为它是可扩展的并且可以应用在所有的容器类型(container type),不仅仅是遵循了STL风格提供了 .begin ()和 .end ()成员函数的容器,甚至数组都可以使用。


如果你使用了一个非STL风格的collection类型,虽然提供了迭代但没有提供STL的 .begin ()和 .end (),通常可以为这个类型编写自己的非成员begin和end来进行重载。这样你就可以使用STL容器的编程风格来遍历该类型。C++11标准提供了示例:C数组就是这样一个类型,标准同时为数组提供了begin和end。


  vector<int> v;
  int a[100];


  // C++98


  sort ( v.begin (), v.end () );


  sort ( &a[0], &a[0] + sizeof(a)/sizeof(a[0]) );


  // C++11


  sort ( begin (v), end (v) );


  sort ( begin (a), end (a) );


Lambda 函数和算法


Lambda[TODO]是决定乾坤的因素,它会使你编写的代码变得更优雅、更快速。Lambda使得STL算法的可用性提高了近100倍。新近开发的C++函数库都是基于lambda可以用的前提(例如,PPL)并且有些函数库甚至要求你编写lambda来调用函数库(例如,C++ AMP)


下面是一个快速示例:找到v里面大于x并且小于y的第一个元素。在C++11中,最简单和干净的代码就是调用一个标准函数。


  // C++98: write a naked loop (using std::find_if is impractically difficult)
  vector<int>::iterator i = v.begin (); // because we need to use i later


  for( ; i != v.end (); ++i ) {


  if( *i > x && *i < y ) break;


  }


  // C++11: use std::find_if


  auto i = find_if ( begin (v), end (v), [=](int i) { return i > x && i < y; } );


想要使用C++编写一个循环或者类似的新特性?不用着急;只要编写一个模板函数(template function)(函数库算法),并且几乎可以将lambda当做语言特性来使用,与此同时会更加灵活,因为它不是固定的语言特性而是一个真正的函数库。


  // C#
  lock( mut_x ) {


  ... use x ...


  }


  // 不使用 lambda 的C++11:已经非常简洁并且更灵活(例如,可以使用超时以及其他选项)


  {


  lock_guard<mutex> hold ( mut_x );


  ... use x ...


  }


  // C++11 with lambdas, and a helper algorithm: C# syntax in C++


  // 使用了 lambda 的C++11可以带一个辅助算法:在 C++ 中使用 C# 的文法


  // Algorithm: template<typename T, typename F> void lock ( T& t, F f ) { lock_guard<T> hold (t); f (); }


  lock( mut_x, [&]{


  ... use x ...


  });


去熟悉lambda吧。你会大量使用它,不仅仅在C++中——它已经广泛应用于很多主流的编程语言。一个开始的好去处请参考我在PDC2010的演讲《无处不在的 lambda》


Move / &&


Move被认为是copy的最佳优化,尽管它也使得其他事情成为可能比如信息被转发。


// C++98:避免 copy 的替代方法
  vector<int>* make_big_vector (); // 选择1: 返回指针: 没有拷贝,但不要忘记 delete


  :::


  vector<int>* result = make_big_vector ();


  void make_big_vector ( vector<int>& out ); // 选择2: 通过引用传递: 没有拷贝,但是调用者需要传入一个有名对象


  :::


  vector<int> result;


  make_big_vector ( result );


  // C++11: move


  vector<int> make_big_vector (); // 通常对于”被调用者(callee)分配的空间“也适用


  :::


  vector<int> result = make_big_vector ();


Move语法改变了我们设计API的方式。我们可以更多地设计通过值传递。为你的类型启用move语法,使用时会比copy更有效。


更多变化


还有更多现代C++的特性。并且我计划在未来编写更多深入C++11新特性以及其他特性的短文,我们会知道更多并且喜欢上它。


但目前,这是必须知道的新特性。这些特性组成了现代C++风格的核心,使得C++代码看起来和执行时像他们设计的那样,你将会看到这些特性会出现在几乎每一段你看到或者编写的现代C++代码中。并且它们使得现代C++更加干净、安全且快速,使得C++在未来的若干年仍然是我们产业的依靠。


主要变更


2011-10-30:为Lambda增加C#lock示例。重新组织智能指针首先介绍unique_prt。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 19楼 发表于: 2011-12-30
ISO发布C语言标准新版本
[简讯]ISO(International Organization for Standardization)正式公布C语言新的国际标准草案。


之前被命名为C1X的新标准将被称为ISO/IEC 9899:2011。新的标准修订了C11版本,提高了对C++的兼容性,并将新的特性增加到C语言中。


新功能包括支持多线程, 基于ISO/IEC TR 19769:2004规范下支持Unicode,提供更多用于查询浮点数类型特性的宏定义和静态声明功能。根据草案规定,最新发布的标准草案修订了许多特性,支持当前的编译器。


今年10月份,ISO ITTF(Information Technology Task Force)开始接手最终草案审查,对此未发表任何评论。 该草案的修改是基于2011年4月通过的N1570 draftPDF。


英文出自:h-online.com
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
如果您在写长篇帖子又不马上发表,建议存为草稿