C++的新特性

介绍

      也许你已经意识到了,在ISO 标准中C++语言已经被更新了。对于新的C++ 语言的编码名字已经改为C++0x, 许多编译器都已经介绍了它的一些特性。这个指南将尝试给你介绍C++ 语言的新特性。请注意,尽管这些特性已经应用到其他的编译器上,但我只在 Visual C++ 2010 编译器上解释一些新的特性。在其他编译器上绝对的语法规则我可不敢解说。

      这篇文章假设你有一定的C++ 语言基础,并且了解类型转换,常量方法,并且知道模板库(基本的意识).

C++的新特性

      以下列表是我将要讨论的C++ 语言增加的一些新特性。我已经更多的强调了lambdas 和R-values (这两个新名词暂时没有合适的汉语翻译)了,由于我还没有在哪里找到任何可消化的东西,所以现在,我将不会使用模板或标准模板库来简单举例,但是我将一定会增加和更新这部分内容的。

关键字:auto keyword

自动数据类型演变(在编译时)依靠分配

关键字:decltype

推断数据类型从表达式或 auto 变量而来

关键字:nullptr

空指针已经被提倡,并且演变成一个关键字!

关键字:static_assert

对于编译时间的断言。对模板和有效性不能使用宏#ifdef 是很有用的

Lambda 表达式

逻辑定义的函数。从函数指针和类对象中继承

Trailing 返回类型

但模板的返回类型不能够表达式,是很有用的

R-value 引用

在模板对象被销毁之前移除语义资源的利用。

其他语言特性

C++ 语言的特性一定被包含在VC8 和VC9 (VS2005, VS2008) 中,在并没有增加进C++ 标准中. 这些特性现在已经被整理到 C++0x中.

这篇文章将详细(但不是全部)介绍这些特性。

让我们开始吧!


关键字 'auto'

auto 关键字,现在有更多的含义。我假设你了解这些关键字的原始意义。使用这个修订的关键字你可以在不需要制定其数据类型的情况下声明一个变量。

例如:

auto nVariable = 16;

以上代码声明了nVariable变量,并没有指定其类型。使用右边的表达式,编译器会判断出变量的类型。像如上的代码会被编译器翻译成如下所示 :

int nVariable = 16;

你可能会断言,对变量的分配现在是强制的。因此,你不能像如下声明一个自动变量:

auto nVariable ; 

// Compilererror:

// errorC3531: 'nVariable': a symbol whose type contains

//              'auto' must have an initializer

现在,编译器就不知道变量nResult 的类型,使用auto关键字:

  • 变量的类型在编译时,而不是在运行时被确定.

已经说过,无论你得任务有多复杂,编译器仍够能确定数据类型。如果编译器不能够判断出类型。它将产生一个错误。这不像是Visual Basic或web脚本语言。

几个例子

auto nVariable1 = 56 + 54; // int 类型的推论

auto nVariable2 = 100 / 3;  // int 类型的推论

 

auto nVariable3 = 100 / 3.0; // double 类型的推论. 由于编译器把整个表达式作为double

auto nVariable4 = labs(-127); // long, 由于 'labs'函数的返回值为long 类型

让我们来点稍微复杂一点的 (继续下面的声明):

// nVariable3被推论为 double.

auto nVariable5 =sqrt(nVariable3);

// 被推论的 double, 因为 sqrt 接受 3 不同的数据类型,但是我们传入double,覆盖返回值为

// double!

 

auto nVariable =sqrt(nVariable4);

// 错误,因为 sqrt 的调用时模棱两可的.

// 这个错误与 'auto' 关键字不相关!

指针的推论:

auto pVariable6 =&nVariable1; // 被推论为'int*', 由于nVariable1 是 int类型.

auto pVariable =&pVariable6; // int**

auto* pVariable7 =&nVariable1; // int*

引用的推论:

auto & pVariable =nVariable1;  //int &

// 是的,又该这个变量的值,将修改其引用的变量!

使用 new 操作符:

auto iArray = new int[10]; // int*

使用 const 和 volatile 修饰符:

const auto PI = 3.14;  // double

volatile auto IsFinished = false;  // bool

const auto nStringLen = strlen("CodeProject.com");

不允许的情景

数组不能被声明自动类型:

 

auto aArray1[10];

auto aArray2[]={1,2,3,4,5};

// errorC3532: 数组类型的的元素的类型不能定义为包含 'auto'

不能为函数的参数或返回类型:

auto ReturnDeduction();

 

void ArgumentDeduction(auto x);

// C3533:'auto': 参数类型的变量不能够被声明包含 'auto'

如果你需要一个 auto的返回类型或auto 的参数,你可要简单的使用模板!

你不能在 类class或结构体struct中使用auto,除非它是一个静态的成员;

struct AutoStruct

{

    auto Variable = 10;

    // errorC2864: 'AutoStruct::Variable' : 只有静态只读的成员变量才可在结构体或类中  被初始化

};

你不能在一个自动变量中包含多种数据类型(类型被推论为不同的类型):

auto a=10, b=10.30, s="new";

// errorC3538: 在一个 declarator-list 'auto'

//             必须被推论成相同的数据类型

像如上,如果你使用不同的函数初始化变量,一个或多个函数返回不同类型的数据类型,编译器一定会产生相同的错误 (C3538如下):

auto nVariable = sqrt(100.0), nVariableX =labs(100);

你可以在全局的层次上使用关键字auto.

这个变量似乎是你得老朋友,什么都懂,但是如果你滥用的话讲是一种诅咒。例如,少部分程序员假想如下的方式声明float ,但实际上为double。

auto nVariable = 10.5;

相似,如果一个函数的返回值为int 。接下来你修改其返回值的类型为short 或double,原来的自动变量将变得小心翼翼。 如果你不幸,只有一个变量使用了auto声明,编译器将推断这个变量为新的数据类型。并且,如果你很幸运, 与其他类型变量混合使用,编译器将会产生C3538的错误(如上)。

因此,我们什么时候应该真正的使用 'auto' 关键字呢?

1. 但一个数据类型依靠编译器和/或目标平台而有可能改变时

For example:

int nLength = strlen("The auto keyword.");

可能在一个32 位的编译环境下返回一个4字节的整型,但是在一个64位的环境下可能返回8位。很确定的事情,你可以使用 size_t代替 int或 __int64。然而没有定义size_t的代码编译会怎样呢!或者 strlen返回别的什么?在这种情况下,你能够简单的使用auto:

auto nLength = strlen("The auto keyword.");

2. 当数据类型的表示比较复杂的时候,或它使得代码很混乱

std::vector<std::string>Strings; // string vector

 // Push severalstrings

 

// 如此显示出它们

for(std::vector<std::string>::iterator iter =

    Strings.begin(); iter !=Strings.end();  ++iter)

{  std::cout <<*iter << std::endl; }

你知道,打出这样的类型std::vector<std::string>::iteratoriter通常是很笨拙的。尽管可以选择使用关键字typedef 在程序的某些地方来返回类型并且使用类型名。但是对于迭代器类型又有多少呢? 并且一个迭代器类型只用一次又会怎样呢? 我们像如下的方式缩短了编码:

 

for(auto iter = Strings.begin(); iter != Strings.end(); ++iter)

{ std::cout << *iter <<std::endl; }

如果你已经有一段时间使用了STL 了,你会了解迭代器和只读迭代器的。如上的例子,你也是会倾向于使用const_iterator来代替iterator。 因此,你也许想这样使用:

// Assume'using namespace std'

for(vector<string>::const_iteratoriter =

          Strings.begin(); iter !=Strings.end(); ++iter)

{ std::cout << *iter <<std::endl; }

因此,使迭代器成为只读,以便于vector的元素不能够被修改。请记住,当你在一个const 的对象上调用 begin方法时, 你不能够再将它分到一个非只读的iterator 上, 并且你必须把它分配到const_iterator上(const iterator和const_iterator不一样, 因为我不是在写STL, 请自己阅读并做相关实验)。

概括了所有的复杂性,并且引进了auto关键字,标准的C++为STL 容器为使用cbegin, cend, crbegincrend方法提供了便利。C 前缀意思为只读。他们总是返回const_iterator的迭代器,不管对象(容器)是否为const。老的方法依靠对象的常量性返回迭代器的两种类型。

// 在cbegin使用的情况下,迭代器总是只读的.

for(auto iter = Strings.cbegin(); iter!=Strings.cend(); ++iter) {...}

另外一个例子在一个迭代器/只读迭代器上:

map<std::vector<int>,string> mstr;

 

map<vector<int>,string>::const_iterator m_iter =mstr.cbegin();

迭代器的声明可以被缩短为如下:

auto m_iter =mstr.cbegin();

就像复杂的模板一样,你可以使用关键字来分配一个难以确定类型,易于出错的函数指针,这些也许可从其他的变量/函数上分配。由于没有例子给出,我假定你能够明白我的意思。 

3. 为变量分配一个 Lambdas(匿名程式)

以下文章是关于lambdas的解释。

4. 为返回类型指定一个Trailing

尽管不涉及到匿名程式,学习(lambda)匿名表达式语法是必须的。我将在匿名程式后面讨论它。

外部引用


'decltype' 关键字

C++ 操作符给出表达式的类型。 例如:

int nVariable1;

...

decltype(nVariable1)  nVariable2;

声明nVariable2为一个int类型的。编译器知道nVariable1的类型,并且将decltype(nVariable1)翻译成int类型。decltype关键字与typeid关键字不同。typeid操作符返回type_info的结构体,并且要求RTTI 是使能的。由于它返回的是类型信息,而不是类型本身,你不能像如下的方式使用typeid:

typeid(nVariable1) nVariable2;

然而,推断出表达式为一个类型,这完全是在编译时刻。你不能够使用decltype获得类型名(如'int')。

decltype与auto结合使用是典型的应用。例如,你声明自动变量如下:

auto xVariable =SomeFunction();

假设xVariable(实际上,为SomeFunction的返回类型)的类型是X.现在,你不能再次调用(或者不想调用)这个函数了。你怎样来声明另外一个相同类型的变量呢?

下面那一个会更适合你呢?

decltype(SomeFunc) yVar;

decltype(SomeFunc())yVar;

第一个声明yVar为函数指针,第二个为类型X。使用函数名字不是很可靠的,你照样可以访问,编译器不会给出错误提示或警告直到你使用变量。通常你必须传递实际的参数值,且函数/方法的参数的实际类型是被覆盖的。

推荐的方式是直接通过类型来推论:

decltype(xVariable) yVar;

况且,因为你已经看到关于auto的讨论了,使用模板类型是复杂和不美观的,你应该使用auto。同理,你应该使用decltype声明一个正确的类型:

decltype(Strings.begin())string_iterator;

decltype(mstr.begin()->second.get_allocator()) under_alloc;

不像是先前的例子那样,然而我们使用auto从右边的表达式上推论出类型, 我们不需要指定的去推论类型。使用decltype,你不需要指定变量,仅仅声明它——因为类型是预先知道的。当你使用表达式Strings.begin()时,函数并没有被调用,它仅仅是从表达式上推论出类型来。相似的,当你把表达式放到decltype中时,表达式并没有被求值。只有基本的语法检查被执行。

在上面的第二个例子中,mstr是std::map对象,我们取回迭代器,即为map元素的第二个成员,并最终为它的分配器类型。因此,为string推断出std::allocator类型(请看上面,mstr被声明)。

几乎没好处,但有点悖理,例子如下:

decltype(1/0) Infinite; // 对于除数为0的情况,编译器并不产生错误

//程序不会 'exit', 会保存 exit(0), 的类型.

//在不进行函数调用的情况下指定exit 会使得'MyExitFunction' 的返回类型有所不同

decltype(exit(0)) MyExitFunction();

外部参考

decltype 的类型来自 - MSDN


关键字 'nullptr'

空指针最终以关键字的形式分类!它和NULL宏和整形0很相似。尽管我只谈论本地C++, 但是要知道,关键字不但用于本地C++, 而且应用于托管的代码中。如果你以混合的模式使用C++, 你可以显式的的使用关键字__nullptr来陈述本地空指针,并且使用nullptr表示托管空指针。即使在混合的模式下编程,你不会常使用__nullptr。

void* pBuffer = nullptr;

...

if ( pBuffer == nullptr )

{ // Do something with null-pointer case }

 

 

// Withclasses

 

void SomeClass::SomeFunction()

{

   if ( this != nullptr)

   { ... }

}

请记住,nullptr是一个关键字,不是类型。因此,对于它你不能使用操作符sizeof或decltype。已经说过,NULL宏和关键字nullptr是不同的两个实体。NULL 就是0,其实就是一个int 类型的。  

例如:

void fx(int*){}

 

void fx(int){}

 

int main()

{

    fx(nullptr);// 调用  fx(int*)

    fx(NULL);   // 调用  fx(int)

 

}

Externalreferences

(使用 /clr 选项编译器要求是错误的. MSDN 没有更新, 写这篇文章之时.)


'static_assert' 关键字

使用static_assert关键字,你能够在编译时指定一些条件。这里是语法规则:

static_assert(expression, message)

表达式要成为一个编译时的常量表达式。对于非模板的静态声明,编译器立刻会认定出表达式来。对于模板的声明,编译器测试断言,什么时候类被实例化。

如果表达式为真,就意味着你要求的断言已经满足,并且陈述不会做任何事情。如果表达式为假,编译器会产生一个C2338 的错误提示。例如:

static_assert (10==9 , "Nine is not equal to ten");

很显然的,不为真,所以编译器会产生如下错误:

error C2338: Nine is notequal to ten

更有意义的断言会产生,当程序没有在32位编译器下编译时:

static_assert(sizeof(void *) == 4,

       "Thiscode should only be compiled as 32-bit.");

由于任何一个指针类型的大小是相同的,当选择目标平台做编译时。

在早期的编译器中,我们需要使用_STATIC_ASSERT,尽管它不会做任何事情,但是声明了数组的大小状况。因此,如果条件为真,它将声明大小为1的数组;如果条件为假, 它将声明大小为0 的数组——这将导致编译器产生一个错误。错误通常不是友好的。

  • error C2466:不能够分配固定大小为0 的数组

外部引用


匿名(Lambda)表达式

这是C++ 增加的一个引人注目的语言特性。非常有用的,有意思的,并且也是复杂的!我将以最基本的语法和例子展开阐述, 使得它变得更加清晰。因此以下几行代码也许不是lambdas的使用特性。但是,可以确定,lambdas 是很有效的,而且也是C++玉雅简洁性的特性!

在开始讨论之前,让我首先概括一下:

  • lambdas 更像一个本地定义的函数。你可以在程序的任何地方去实现一个lambdas,可以像代替正常的表达式或像正常的函数一样被调用(想起来了在之前的VC++ 编译器中会报错“本地函数的定义是非法的?”)。

最最基本的lambda:

 []{};  // 在某些函数中/或代码块中 而不是全局的.

是的,上面的声明是完全合法的 (只在 C++0x!).

[]是匿名表达式的引入符号,这个符号告诉编译器下面的表达式是一个匿名的。{}是匿名表达式的定义部分,就像任何函数/方法一样。以上的匿名表达式没有传递任何参数,不会返回任何值,当然也不会做任何事情。

让我们继续...

 []{ return 3.14159; }; // 返回一个 double

以上的匿名代码可以简单的工作:返回PI的值。但是谁会调用这个lambda呢?返回值又会去哪里呢? 让我们进一步来讨论:

double pi = []{ return 3.14159; }(); // Returnsdouble

你意识到了吗? 返回值被存储到一个局部变量pi中. 通常, 注意上面的例子,匿名函数被调用(注意最终的函数调用). 下面是最少的运行程序代码:

int main()

{

   double pi;

   pi = []{return3.14159;}();

   std::cout <<pi;

}

匿名表达式的最后一个圆括号是对匿名函数的一个调用。 这里,匿名表达式没有携带任何参数,但是操作符()在其结尾仍然需要,以便于让编译器知道这是一个调用。pi的匿名表达式也许可以像如下的方式被实现:

pi = [](){return 3.14159;}(); // 注意,第一个圆括号.

这个表达式更像:

pi = [](void){return 3.14159;}(); // 请注意 'void'

尽管,你是否在第一个圆括号中放入参数是一个选择。但是我更加倾向于你放入参数。 C++标准委员会想使得匿名表达式看起来稍微简单一点,这就(也许就)是他们把匿名参量作为可选参量的原因吧。

让我们更近一步,带着匿名参数来讨论:

bool is_even;

is_even = [](int n) { returnn%2==0;}(41);

第一个圆括号,(int n),指定了匿名表达式的参量。第二个圆括号,(41),给匿名表达式传进一个值。匿名表达式的主体部分的功能为判断传入的数字是否能被2整除。现在我们可以实现一个判断最大,或最小值的匿名表达式,如下所示:

int nMax = [](int n1, int n2) {

return (n1>n2) ? (n1) : (n2);

} (56, 11);

 

int nMin = [](int n1, int n2) {

return (n1<n2) ? (n1) : (n2);

} (984, 658);

这里,不去独立的声明和分配变量,我把他们放到同意行。匿名表达式现在接收两个参数,返回其中一个,这个值被分配给nMin 或nMax. 同理,匿名表达式可以传进更多的参数,并且也可接收更多参数类型。

你应该会有几个问题:

  • 返回值是什么呢? 只有 int 是有效的吗?
  • 匿名表达式不止一个返回状态又会怎样呢?
  • 匿名表达式需要做别的事情,如显示一个值,执行别的程序步骤又会怎样呢?
  • 我能否把一个引用存储到定义的匿名表达式中,并且在别的地方再次引用呢?
  • 一个匿名表达式能否调用另一个匿名表达式或函数呢?
  • 一个匿名表达式被定义成一个局部函数,它能否穿越函数的作用域呢?
  • 一个匿名表达式可以在一个变量定义或调用的地方访问这个变量呢?能修改该这个变量的值吗?
  • 它支持缺省参数吗?
  • 它与函数指针或函数对象(函子)有怎样的不同呢 ?

在我一个个回答如上问题时,请让我使用如下的列表来向你展现匿名表达式的语法规则:

Q. 关于返回值是什呢?

你可以在操作符 -> 后面指定返回值类型。例如:

pi = []()->double{ return 3.14159; }();

请记住,如上表所示,如果匿名函数仅包含了一条陈述(即,只有一个返回语句),就不需要指定返回类型。因此,在上面的例子中,指定返回值为double是可选的。对于自动可推论的返回类型你是否显式的指定其返回类型完全由你决定。

一个必须强制指定返回类型的例子:

int nAbs = [] (int n1) -> int

{

    if(n1<0)

       return-n1;

    else

       returnn1;

}(-109);

如果你没有指定 -> int, 则编译器会产生:

error C3499: 一个匿名表达式已经被指定成一个空的返回类型不能够再返回一个值

如果编译器一个return声明都没有发觉:它就推论匿名函数有一个空的返回类型。这个返回类型可以是任何类型。

 []()->int*{ }

[]()->std::vector<int>::const_iterator&{}

[](int x) ->decltype(x) { }; // Deducing type of 'x'

它不能返回一个数组。也不能有一个自动变量类型的返回值:

 []()-> float[] {};  // error C2090: 函数返回了数组

[]()-> auto {};    //error C3558: 'auto': 匿名函数的返回值不能包含 'auto'

当然,你可以使用一个自动变量来接收匿名函数的返回值:

auto pi = []{return 3.14159;}();

 

auto nSum = [](int n1, int n2, int n3)

{ return n1+n2+n3; } (10,20,70);

 

auto xVal = [](float x)->float

{

    float t;

    t=x*x/2.0f;

return t;

} (44);

最后一点,如果你为有参量的匿名函数指定一个返回类型,你必须使用圆括号,下面的代码将产生错误:

 []->double{return 3.14159;}();  // []()->double{...}

Q. 匿名函数不止一个返回表达式会是什么情况呢?

上述的解释已经足够的说明了匿名函数能够包含任何常规函数所包含的代码。匿名函数能够包含任何函数\方法所包含的——局部变量,静态变量,调用其它函数,内存分配,和其他匿名函数!如下的的代码是有效的(尽管有点荒诞!):

[]()

{

   static int stat=99;

   classTestClass

   {

      public:

        intmember;

   };

 

   TestClass test;

   test.member= labs(-100);

 

 

   int ptr =[](int n1) ->int*

   {

      int* p = new int;

      *p = n1;

      return p;

   }(test.member);

 

   delete ptr;

};

Q. 匿名函需要做一些事情,如显示一个值,执行别的一些指令将会是怎样的呢?我能否把一个引用存储到一个已经定义的匿名函数中,并在一些地方再次使用? 一个匿名函数被定义成一个局部函数,能否穿越函数作用域来使用呢?

让我们定义一个匿名函数来判断一个数值是否为偶数。使用auto关键字,我们能够把匿名函数存储在变量中。因此我们可以使用这个变量(也就是说,一个对匿名函数的调用)!我接下来会讨论匿名函数的类型。像如下的简单的定义:

auto IsEven = [](int n) -> bool

{

     if(n%2 == 0)

        return true;

     else

        return false;

};  // 没有函数调用

就像你猜测的那样,匿名函数的返回值为一个 bool类型的,它接收一个参数。并且很重要的是,我们没有调用匿名函数, 只是定义了它。如果我们使用()并携带参数 ,变量的类型应该为bool类型,并不是匿名函数类型!如上声明后,现在局部定义的匿名函数可以被调用。

IsEven(20);

 

if( ! IsEven(45) )

      std::cout <<"45 is not even";

对IsEven的定义,如上已经给出,在同一个函数中会有两次调用。如果你想在别的函数中来调用匿名函数会是怎样的情况呢? 而这里有一种应用,如存储进一些局部或类级别的变量中,然后把它传进另外一个函数(就像一个函数指针),并且从别的函数中调用。另一种机制是以全局的范围来存储和定义一个函数。由于我们没有讨论匿名类型的含义,我们将稍后讨论第一种应用,下来让我们讨论第二种应用吧(全局范围):

Example:

// 匿名函数的返回值为 bool

// 匿名函数被存储到IsEven, 使用自动类型变量

auto IsEven = [](int n) -> bool

{

   if(n%2 == 0) return true;

   else return false;

}

 

void AnotherFunction()

{

   // 调用它!

   IsEven (10);

}

 

int main()

{

    AnotherFunction();

    IsEven(10);

}

由于auto是可用于局部和全局范围的,我们能够使用它存储匿名表达式。我们需要知道类型以便于能够把它储存到类变量中,接下来使用。

早先已经说过,匿名函数几乎能够做常规函数所做的一切,因此显示一个值对匿名函数来说并不是遥不可及的事情。

int main()

{

    using namespace std;

 

    auto DisplayIfEven= [](int n) -> void

    {

        if (n%2 == 0)

            std::cout <<"Number is even\n";

        else

            std::cout <<"Number is odd\n";

    }

   

    cout <<"Calling lambda...";

    DisplayIfEven(40);

}

一个重要的结论要注意——一个局部定义的匿名函数不能够从它被定义进的上一级作用域中获得命名空间的解决方案。因此std命名空间的范围对于DisplayIfEven是不可用的。

Q. 一个匿名函数能否包含另外一个匿名函数或常规函数吗?

已经很明确,你已经知道了提供一个匿名表达式/函数的名字是在调用的时刻,就像函数的调用要在函数中一样的要求。

Q. 匿名函数支持缺省参数吗?

不。

Q.一个匿名函数表达式可以从它定义或调用的地方访问变量吗?能否修改变量内容?它与函数指针,或函数对象(函子)有什么不同吗?

现在我将讨论我遗留的问题:捕获说明。

匿名表达式可以是如下的情况:

  • 状态的
  • 无状态的

状态定义了变量在一个更高一级的作用域中是怎样被捕获的。我从下面的类别中明确它:

  1. 没有变量在它的上一级作用域中被访问。这条我们一直在使用.
  2. 变量在只读模式下被访问。你不能修改上一级变量的值。
  3. 变量被拷贝到匿名函数中(以相同的名字),你可以修改拷贝的变量,这类似于函数的传值调用机制。
  4. 对于上一级的变量作用域,你拥有完全的访问权限,你可以使用相同的名字修改变量。

你会明白以下4条是继承至C++ 的优点:

  1. private变量, 你无法访问它。
  2. const (常量)方法中, 你不能修改变量。
  3. 对于函数/方法的变量是通过传值的。
  4. 变量在方法中是完全访问的。或者说,变量是通过引用传递的。

让我们来介绍一下捕获!捕获的说明在上面的图中已经提到,是通过[]给出的。下面的语法用于指定捕获规格说明:

  •  [] – 什么都不会捕获。
  • [=] – 通过值来捕获。
  • [&] – 通过引用来捕获。
  • [var] – 通过值来捕获 var。
  • [&var] – 通过引用来捕获var。

例 1:

int a=10, b=20, c=30;

 

[a](void)  // 仅通过值来捕获 'a'

{

    std::cout <<  "Value ofa="<<

    a <<std::endl;

       

    // 不能够修改

    a++; // error C3491: 'a': a 是通过值来捕获的

          //       在一个非异变的匿名表达式中不能够被修改

 

    // 不能访问其他变量

    std::cout <<b << c;

    // errorC3493: 'b'不能够被隐式的捕获

// 因为没有缺省的捕获模式被指定

}();

例 2:

auto Average = [=]() -> float  // '=' 意思是: 通过值来捕获所有变量

{

    return ( a+ b + c ) / 3.0f;

 

   // 不能修改任何变量的值

};

 

float x = Average();

例 3:

// 使用 '&' 你指定了所有的变量通过引用来捕获

auto ResetAll =[&]()->void

{

    // 由于你是通过引用来捕获变量的,你就不能够修改他们!

    a = b = c = 0;

};

 

ResetAll();

// a,b,c 的值被设为0;

放置一个 = 指定通过值来捕获。放置一个 & 指定通过引用来捕获。 现在让我们进行更多的探索, 言简意赅, 我并不是把匿名表达式放进一个自动变量中再去调用它。相反,我直接引用。

例 4:

// 通过值仅捕获 'a' 和 'b'

int nSum = [a,b] // 你还记得 () 对于无参数的匿名函数是可选的吗?

{

    return a+b;

}();

 

std::cout << "Sum: "<< nSum;

如例4 所示,我们使用匿名表达式的引入符号([]操作符)可以进行多个参数的捕获。我来举出第二个例子, 而所有的三个参数(a, b, c)的和被存储在nSum中。

例 5:

// 只有'nSum'是通过引用捕获,其他均通过值捕获

[=, &nSum]

{

    nSum = a+b+c;

}();

在上例中,通过值来捕获所有变量(即=操作符)指定了缺省的捕获模式,表达式&nSum 覆盖了它。注意,缺省的捕获模式对所有的捕获必须出现在其他捕获之前。因此=&必须出现在其他指定之前,如下声明将引发错误:

// & 或 = 必须首先出现(如果被指定).

[&nSum,=]{}

[a,b,c,&]{} // 逻辑上与上述相似,但是出错的.

几个例子:

 [&, b]{}; //(1) 通过引用来捕获,除过‘b’为值捕获之外

 [=, &b]{}; //(2) 与上述相反

[b,c, &nSum]; // (3) 通过值来捕获‘b’,‘c’

                  //'nSum' 为值捕获.

[=](int a){} // (4) 所有通过值捕获,隐藏了 'a' – 因为 a 现在是一个函数的参数,糟糕的练习,//编译器不会产生警告            

[&, a,c,nSum]{}; // 与 (2) 相似

[b, &a, &c,&nSum]{} // 与 (1)相似

[=, &]{} // 无效!

[&nSum, =]{} // 无效!

[a,b,c, &]{} // 无效!

就像你看到的那样,对于相同集的变量的捕获是多种结合方式的。我们可以通过增加如下的方式来扩展指定的捕获:

  • [&,var] – 除过var是通过值捕获的,其他均通过引用捕获。
  • [=, &var] -除过var是通过引用捕获的,其他均通过值来捕获。
  • [var1, var2] – 通过值来捕获变量var1, var2  。
  • [&var1, &var2] – 通过引用来 var1, var2 。
  • [var1, &var2] – 通过值来捕获 var1 ,引用来捕获 var2 。

到现在,我们已经看到了我们可以防止一些变量被捕获,防止通过值的关键字为const,防止通过引用的关键字non-const 。因此,在上面的捕获类别中我们已经覆盖了1,2 和4。捕获constreference是不可能的(即,[const&a])。我们现在将会去研究一下最后一个在通过值调用模式的捕获。

'mutable' 的说明

在参数说明括号后面,我们指定了一个mutable关键字。我们把所有通过值来捕获的变量放入一个通过值调用模式。如果我们不放置一个mutable关键字,所有通过值来捕获的变量都是只读的,你不能在匿名函数中修改它。放置一个关键字mutable就告诉编译器强制拷贝所有通过值来捕获的变量。因此你接下来可以修改通过值捕获的变量了。 没有那种方法能否选择的通过值来捕获const 或非const 变量。 或者简单点,你可以假定他们通过传参的形式传入匿名函数。

例如:

int x=0,y=0,z=0;

 

[=]()mutable->void // ()是必须的, 当我们指定一个 'mutable'关键字时

{

    x++;

// 因为所有变量都是在通过值调用的模式下捕获

// 编译器会产生一个警告,由于 y,z 均未使用

}();

// x的值依然为0

匿名函数调用后,x的值仍然是0,因为仅仅只是对x的一个拷贝做了修改,而不是引用。编译器仅对y和z 产生而不是先前定义的变量(a, b, c...)产生一个告警通常也是很有趣的一件事情。然而,它不会抱怨你是使用了预先定义的变量。智能的编译器——我不敢说会使什么结果!

匿名函数与函数指针,函数对象有怎样的区别呢?

函数的指针不保留状态,而匿名函数保留。通过引用的捕获,匿名函数可以在调用期间保留它们的状态。函数却不能。 函数指针不是类型安全的,他们是容易出错的,我们必须严格遵守调用约定并且要求复杂的语法。

函数对象也保存状态。但是甚至一个小的应用程序,你必须写一个类,并且在类中定义一些变量,并且重载操作符()。更重要的是,你必须在函数块外面做这些工作以便于其他对于这个类应该调用operator()的函数必须知道他。这样破坏了代码的的流程。

匿名表达式的类型是什么呢?

匿名表达式其实就是一些类。 你能够把它们存储在一个function 类对象中。这个类,对于匿名表达式,被定义在std::tr1命名空间中。让我们看一个例子:

#include<functional>

....

std::tr1::function<bool(int)>s IsEven = [](intn)->bool { returnn%2 == 0;};

...

IsEven(23);

tr1命名空间在技术报告1中,技术报告1被C++0x 委员会使用,你可以自己去找更多的信息。 <bool(int)>表示了对于function类的模板参数,意思是说:函数返回一个bool 类型,传入一个int 类型。 基于匿名表达式被放到函数对象中这样的机制,你必须正确的进行类型转换;否则,编译器会因为类型不匹配而产生错误或警告。但是,就像你能够看到的那样,使用auto关键字会更加的方便。

这里有一些用例,然而,你在哪里必须使用一个function —— 当你需要穿过函数传递匿名表达式时. 如下例:

using namespace std::tr1;

void TakeLambda(function<void(int)> lambda)

// 不能够在函数参数中使用 'auto'

{

    // 调用它!

    lambda(32);

}

 

// 在程序的某些地方 ...

TakeLambda(DisplayIfEven);// 看上述代码 'DisplayIfEven'

DisplayIfEven 匿名表达式 (或函数!) 接受 int, 返回空。  function 类以相同的方式被作为参数在TakeLambda中使用。进一步,它调用匿名表达式, 匿名表达式最终调用DisplayIfEven 匿名表达式.

我已经简化了 TakeLamba,此表达式应该为 (逐渐展示):

// 引用,不应当拷贝'函数'对象

void TakeLambda(function< void(int) > & lambda);

 

// 只读的引用,不应该修改函数对象

void TakeLambda(const function< void(int) > &lambda);

 

// 完全限定名

void TakeLambda(const std::tr1::function< void(int) > &lambda);

在 C++中介绍匿名表达式的目的是什么?

匿名表达式对于许多STL 函数时非常有用的——即函数要求一个函数指针或函数对象(使用operator()重载)。简而言之,匿名表达式对于要求回调函数的程序历程是非常有用的。起初,我没去覆盖STL 的函数,但是以一种简单的容易理解的形式解释了匿名表达式的用处。非STL 的例子也许显得很多余,但却能够帮助澄清这个主题。 

例如, 下面的函数的参数要求传入一个函数。 它将调用传进的函数。函数指针,函数对象,或者匿名表达式应该是需要的类型,返回一个void类型并且接受一个int类型的参数作为唯一的参数。 

void CallbackSomething(int nNumber, function<void(int)> callback_function)

{

    // 调用指定的 '函数'

    callback_function(nNumber);

}

这里我以三种不同的方式调用 CallbackSomething 函数: 

// 函数

void IsEven(int n)

{

   std::cout <<((n%2 == 0) ? "Yes": "No");

}

 

// 类对操作符() 的重载

class Callback

{

public:

   void operator()(int n)

   {

      if(n<10)

         std::cout <<"Less than 10";

      else

         std::cout <<"More than 10";

   }

};                                     

 

int main()

{

   // 传递一个函数指着

   CallbackSomething(10,IsEven);

 

   // 传递一个函数对象

   CallbackSomething(23,Callback());

 

   // 另外一种方式..

   Callback obj;

   CallbackSomething(44,obj);

 

   // 本地定义的匿名表达式!

   CallbackSomething(59,[](int n)   { std::cout << "Half: " <<n/2;}     );

}

好! 现在我想让 类呢个能够显示一个数字是否大于N(替代一个常量10)。 我们可以这样来完成这个任务:

class Callback

{

   /*const*/int Predicate;

public:

   Callback(intnPredicate) : Predicate(nPredicate) {}

 

   void operator()(int n)

   {

      if( n < Predicate)

         std::cout <<"Less than " << Predicate;

      else

         std::cout <<"More than " << Predicate;

   }

};

以便于能使得这样可调用,我们需要一个整型常量来构造它。原始的函数CallbackSomething不需要被改变——它仍然能够携带一个整型参数来调用程序历程!这就是我们怎样做它方法:

// 传进一个函数对象

CallbackSomething(23, Callback(24));

// 24 是CallBack 构造函数的参数,不是CallbackSomething!的参数

 

// 另一种方式..

Callback obj(99); // 设置 99 去判断

CallbackSomething(44, obj);

这个方式,我们使得Callback类拥有保存状态的能力。记住,只要对象在,它的状态就保持。因此,如果你把一个 obj对象传进多个CallbackSomething的调用(或者任何其他相似的函数),它将拥有相同的断言(状态)。 就像你所了解的那样,这在函数指针中绝对是不可能的——除非我们对函数引入另外一个参数。但是,这样做破坏了程序的整个结构。如果一个特殊的函数需要一个携带制定类型的可调用的函数,我们只能穿几这个类型的函数。函数指针不能保存状态,因此在这类场景下不可用。

使用匿名表达式对于这样的情况有可能吗?如前面提到的那样,匿名表达式能够通过指定的捕获保存状态。因此,的确,使用匿名表达式能够完成这样包含状态的功能。这里是一个被修改的匿名表达式,被存储在auto变量中:

int Predicate = 40;

 

// 匿名表达式被存储在 ' 有状态的' 变量中

auto stateful  = [Predicate](intn)

   {  if( n < Predicate)

           std::cout <<"Less than " << Predicate;

         else

           std::cout <<"More than " << Predicate;

   };

 

CallbackSomething(59, stateful ); // Morethan  40

   

Predicate=1000;

CallbackSomething(100, stateful); // Predicate NOT changed for lambda!

能保持状态的匿名表达式被本地的定义在一个函数中,与函数对象比较更加的简洁,并且比函数指针要清晰多。现在它拥有一个状态。因此第一次调用会打印出“More than 40”,第二次调用相同。

注意,断言值是通过传值调用(非易变的),因此修改原始值不会影响到它在匿名表达式中的状态。为了反映在匿名表达式中对断言的修改,我们仅仅需要通过引用来捕获变量就OK。当我们改变匿名表达式如下,第二个调用将打印“Less than 1000”。

auto stateful  = [&Predicate](intn) // 通过引用来

这与在一个类中增加一个方法如 的机制很相似,这个类可以修改断言的值(状态)。请查看VC++ 的博客,如下链接,会谈到匿名表达式——类的映射。

使用STL

STL的for_each函数会在一个范围内/集合中为每一个元素调用指定的函数,因为它使用模板,它能够接受任何类型的数据类型作为他的参数。我们可以使用这个特性作为匿名表达式的一个例子。更简单的,我将使用基本的数组,而不是向量或列表,例如下所示:

using namespace std;

    

int Array[10] = {1,2,3,4,5,6,7,8,9,10};

 

for_each(Array,&Array[10], IsEven);

 

for_each(Array, Array+10,[](int n){ std::cout <<n << std::endl;});

第一个调用调用IsEven函数,第二个调用调用匿名表达式,此表达式在for_each函数中定义。它会把这两个函数调用10次,因为数组的范围包含/指定了10个元素。对于for_each函数是完全相同的我不需要再重复它第二个参数了(哦!但还是说了)。

这是一个非常简单的例子,for_each和匿名表达式不需要写一个函数或类的情况下就可以被利用来能显示值。 值得肯定,匿名表达式能被进一步扩展用来做额外的工作——如打印是个数是否为素数,或者计算求和(通过使用引用调用),或修改一个范围的元素。

修改匿名表达式的形参?

对,是的,你能做到它,我一直在讨论通过引用来捕获并做出修改,但是没有涵盖修改自身的参数。这个需求一直都没有出现直到现在。为了这样做,仅通过引用(或指针)接受匿名表达式的参数:

Well, yes! You can do that. For long, I talked abouttaking captures by references and making modifications, but did not covermodifying the argument itself. The need did not arise till now. To do this,just take the lambda's parameter by reference (or pointer):

// 'n' 被通过引用传递 (与通过引用捕获不相同!)

for_each(Array, Array+10,[](int& n){ n *= 4; });

以上 for_each 的调用会对数组 Array 的每个元素乘以 4.

就像我解释的怎样利用for_each函数来使用匿名表达式一样,你可以使用其他<algorithm>函数如 transform,generate,remove_if等等来使用它。匿名表达式不仅仅局限于使用STL 功能。他们也能够被很有效率的适用于任何你所需求的函数对象。你需要确保给他传递正确个数,类型的参数,检查它是否需要参数修改和上述的需要。因为这个文档讲的并不是STL 和模板,所以我不再进一步谈论这个。

一个匿名表达式不能被当作函数指针来使用

是的,很失望和迷惑吧,但是却是事实!你不能够把一个匿名表达式当作参数传进一个参数为函数指针的函数。尽管有样本代码,让我首先解释我到底想要表达什么:

// 定义一个函数指针类型,有一个int 型的参数

typedef void (*DISPLAY_ROUTINE)(int);

 

// 定义一个函数,以一个函数指针作为参数,

void CalculateSum(int a,int b, DISPLAY_ROUTINE pfDisplayRoutine)

{

   // 调用这个函数指针

   pfDisplayRoutine(a+b);

}

CalculateSum接收一个DISPLAY_ROUTINE的函数指针类型. 下面的代码可以工作,因为我们给他了一个函数指针:

void Print(int x)

{

  std::cout << "Sum is: " <<x;

}

 

int main()

{

   CalculateSum(500,300, Print);

}

但是下面的代码会有问题:

CalculateSum (10, 20, [](int n) {std::cout<<"sum is: "<<n;});

// C2664:'CalculateSum' : cannot convert parameter 3 from

//        '`anonymous-namespace'::<lambda1>'to 'DISPLAY_ROUTINE'

为什么? 因为匿名表达式是面向对象的,他们实际就是类。编译器对于匿名表达式内部产生了一个类的模型。内部产生的类会对操作符()重载; 并会拥有一些数据成员(通过捕获指定和易变指定来推断)——这些也许是只读,引用,或常规的成员变量和类的填充。这样的类不能够被降级成一个常规的的函数指针。

先前的例子是怎样运行的呢?

好,因为智能的类std::function!看(上面)CallbackSomething实际把一个函数当成参数,而并不是函数指针。

      如for_each——这个函数不会传递std::function,而是使用模板来代替。他直接调用括号内传递参数。认真的理解简化的实现:

template <class Iteartor, class Function>

void for_each(Iteartor first, Iterator, Function func)

// 忽略返回值和其他参数

{

  // 假设下面的调用是一个循环

  // 'func' 可以使一个常规函数,或可以是一个类的对象,操作()符被重载

 

  func(first);

}

同理,其他的STL 函数,如,,等等,可以工作在三种情况下:函数指针,函数对象,匿名表达式。

      因此,如果你计划在API 函数中使用匿名表达式,如SetTimer, EnumFontFamilies,等等——别再计划了!即使强制类型转换匿名表达式(通过传递它的地址),也不会工作,程序会在运行时崩溃。

猜你喜欢

转载自blog.csdn.net/besidemyself/article/details/6707307