65.9K
CodeProject 正在变化。 阅读更多。
Home

解释新的 C++ 标准 (C++0x) 及其在 VC10 中的实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (152投票s)

2010 年 4 月 8 日

CPOL

40分钟阅读

viewsIcon

324005

downloadIcon

1099

通过清晰、准确、详细的讨论,阐述新的 C++ 语言特性。

引言

您可能已经知道,C++ 语言正在通过 ISO 标准进行更新。新 C++ 语言的代号是 **C++0x**,许多编译器已经引入了其中的一些特性。本教程旨在为您介绍 C++ 语言的新变化。请注意,我仅为 **Visual C++ 2010** 编译器解释新特性,尽管它们也适用于其他编译器。我不会评论其他编译器的绝对语法。

本文档假设您对 C++ 有中等水平的知识,并且您知道什么是类型转换、什么是 const 方法以及什么是模板(基本意义上)。

C++ 新特性

以下是我将要讨论的新 C++ 语言特性的列表。我对 lambda 和 R 值给予了更多关注,因为我觉得找不到任何易于理解的内容。目前,为了简单起见,我没有使用模板或 STL,但我可能会更新本文档以 *添加* 关于模板/STL 的内容。

auto 关键字 用于在编译时根据赋值自动推导数据类型。
decltype 关键字 用于从表达式或 auto 变量推导数据类型。
nullptr 关键字 空指针现在得到了提升,并拥有了自己的关键字!

static_assert 关键字

用于编译时断言。对于模板和无法通过 #ifdef 完成的验证很有用。
Lambda 表达式 局部定义的函数。继承了函数指针和类对象(仿函数)的特性。
尾随返回类型 主要用于模板函数的返回类型无法表达的情况。
R 值引用 移动语义 - 在 *临时* 对象销毁之前进行资源利用。
其他语言特性

C++ 语言特性,这些特性已经包含在 VC8 和 VC9 (VS2005, VS2008) 中,但未被添加到 C++ 标准中。这些特性现在被归类到 C++0x 中。

本文档简要解释了它们(并非全部)。

让我们开始吧!


'auto' 关键字

auto 关键字现在有了 *另一个含义*。我假设您知道此关键字的原始用途。有了修订后的含义,您可以在不指定变量数据类型的情况下声明局部变量。

例如

auto nVariable = 16;

上面的代码在不指定类型的情况下声明了 nVariable 变量。通过右侧的表达式,编译器会推导出变量的类型。因此,上面的代码会被编译器翻译为

int nVariable = 16;

正如您所能推断的,变量的赋值现在是强制性的。因此,您 **不能** 像这样声明 auto 变量:

auto nVariable ;  
// Compiler error:
// error C3531: 'nVariable': a symbol whose type contains
//              'auto' must have an initializer

在这里,编译器不知道(也不能知道)nResult 变量的数据类型。请注意,使用 auto 关键字时

  • 变量的数据类型在编译时确定,而不是在运行时确定。

话虽如此,无论您的赋值有多复杂,编译器仍会确定数据类型。如果编译器无法推导出类型,它将发出错误。这 **不像** Visual Basic 或 Web 脚本语言。

一些示例

auto nVariable1 = 56 + 54; // int deduction
auto nVariable2 = 100 / 3;  // int deduction
 
auto nVariable3 = 100 / 3.0; // double deduction. 
// Since compiler takes the whole expression to be double
auto nVariable4 = labs(-127); // long, Since return type of 'labs' is long

让我们稍微复杂化一点(继续使用上面声明的变量)

// nVariable3 was deduced as double.
auto nVariable5 = sqrt(nVariable3);
// Deduced as double, since sqrt takes 3 different datatypes,
// but we passed double, and that overload returns double!

auto nVariable = sqrt(nVariable4); 
// Error since sqrt call is ambiguous.
// This error is not related to 'auto' keyword!

指针推导

auto pVariable6 = &nVariable1; // deduced as 'int*', since nVariable one is int.
auto pVariable = &pVariable6; // int**
auto* pVariable7 = &nVariable1; // int*

引用推导

auto & pVariable = nVariable1;  // int &
// Yes, modifying either of these variable with modify the other! 

使用 new 运算符

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

使用 constvolatile 修饰符

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};
// error C3532: the element type of an array
//              cannot be a type that contains 'auto'

不能作为函数参数或返回类型

auto ReturnDeduction(); 

void ArgumentDeduction(auto x);
// C3533: 'auto': a parameter cannot have a type that contains 'auto'

如果您需要 auto 返回类型或 auto 参数,您只需使用模板!;)

您不能在 classstruct 中拥有 auto,除非它是静态成员

struct AutoStruct 
{
    auto Variable = 10; 
    // error C2864: 'AutoStruct::Variable' : only static const
    // integral data members can be initialized within a class
};

您不能拥有多个数据类型(可以推导出不同类型的类型)

auto a=10, b=10.30, s="new"; 
// error C3538: in a declarator-list 'auto'
//              must always deduce to the same type

同样,如果您用不同的函数初始化变量,并且一个或多个函数返回不同的数据类型,编译器将发出相同的错误(C3538)。

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

您*可以* 在全局级别使用 auto 关键字。

这个变量对您来说可能是一个福音,但如果滥用,它可能是一场灾难。例如,一些程序员假设以下声明是 float,但实际上声明的是 double

auto nVariable = 10.5; 

同样,如果一个函数返回 int,然后您修改函数返回类型以返回 shortdouble,原始的 *自动* 变量定义将失效。如果您*运气不好*,并且只在一个 auto 声明中放置了一个变量,编译器将仅用新类型推导出该 auto 变量。如果您运气好,并且将该变量与其他变量混合使用,编译器将发出 C3538 错误(见上文)。

那么,我们到底应该什么时候使用 'auto' 关键字?

1. 当数据类型可能因编译器和/或目标平台而异时

例如

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

在 32 位编译中将返回一个 4 字节整数,但在 64 位编译中将返回一个 8 字节 int。当然,您可以使用 size_t,而不是 int__int64。如果代码在 size_t 未定义的地方编译,或者 strlen 返回其他内容怎么办?在这种情况下,您可以使用 auto

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

2. 当数据类型难以表达,或者会使代码混乱时

std::vector<std::string> Strings; // string vector
 // Push several strings

// Now let's display them
for(std::vector<std::string>::iterator iter = 
    Strings.begin(); iter != Strings.end();  ++iter)
{  std::cout << *iter << std::endl; }

您知道输入 *类型 std::vector<std::string>::iterator iter* 是繁琐、易错且使代码可读性降低的。尽管存在一个选项,可以在程序其他地方使用 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_iterator iter = 
          Strings.begin(); iter != Strings.end(); ++iter)
{ std::cout << *iter << std::endl; }

从而使*迭代器*成为 const,这样它就不能修改 vector 的元素。请记住,当您在 const 对象上调用 begin 方法时,您不能将其赋给非 const iterator,而必须将其赋给 const_iterator(*const iteratorconst_iterator 不同;由于我没有写关于 STL 的内容,请自行阅读/实验)。

为了克服所有这些复杂性,并帮助 auto 关键字,标准 C++ 现在为 STL 容器提供了 cbegincendcrbegincrend 方法。c 前缀表示常量。它们总是返回 const_iterator,而不管对象(容器)是否是 const。旧方法根据对象的 const 性返回两种类型的迭代器。修改后的代码

// With cbegin the iterator type is always constant.
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();

就像复杂的模板一样,您可以使用 auto 关键字来赋值难以输入、易出错的函数指针,这些函数指针可能从其他变量/函数赋值。我假设您理解我的意思,因此不提供示例。

3. 为 Lambda 赋值给变量

请参阅本文档下方关于 lambda 的解释。

4. 指定*尾随返回类型*

虽然与 lambda 无关,但学习 lambda 表达式语法是必需的。所以我将在 lambda 之后讨论这一点。

外部参考


'decltype' 关键字

这个 C++ 运算符给出表达式的类型。例如

int nVariable1;
...
decltype(nVariable1)  nVariable2;

声明 nVariable2int 类型。编译器知道 nVariable1 的类型,并将 decltype(nVariable1) 翻译为 intdecltype 关键字与 typeid 关键字*不同*。typeid 运算符返回 type_info 结构,并且还需要启用 RTTI。由于它返回类型信息,而不是类型本身,因此您不能像这样使用 typeid

typeid(nVariable1) nVariable2;

decltype 则完美地在编译时将表达式推导为类型。您不能用 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 对象,我们从中检索迭代器,该映射元素的 second 成员,最后是它的分配器类型。因此,*推导出的类型*对于 stringstd::allocator(参见上面 mstr 的声明)。

一些*好*的,尽管*荒谬*的例子

decltype(1/0) Infinite; // No compiler error for "divide by zero" !

// Program does not 'exit', only type of exit(0), is determined.
// Specifying exit without function call would make
// the return type of 'MyExitFunction' different!
decltype(exit(0)) MyExitFunction(); 

外部参考


'nullptr' 关键字

空指针终于以关键字的形式得到了它的分类!它与 NULL 宏或整数 0 大致相同。尽管我只涵盖原生 C++,但仍需提及,nullptr 关键字可用于原生(非托管)代码以及托管代码。如果您在 C++ 中编写混合模式代码,可以使用 __nullptr 关键字显式指定原生空指针,而 nullptr 则用于表示托管空指针。即使在混合模式程序中,您也很少需要使用 __nullptr

void* pBuffer = nullptr;
...
if ( pBuffer == nullptr )
{ // Do something with null-pointer case }

 
// With classes

void SomeClass::SomeFunction() 
{
   if ( this != nullptr)
   { ... }
}

请记住,nullptr 是一个关键字,而不是一个类型。因此,您不能对其使用 sizeofdecltype 运算符。话虽如此,NULL 宏和 nullptr 关键字是两个不同的实体。NULL 只是 0,而 0 就是 int

例如

void fx(int*){}

void fx(int){}

int main()
{
    fx(nullptr); // Calls fx(int*)
    fx(NULL);    // Calls fx(int)

}

外部参考

  • nullptr 关键字 - MSDN
  • (/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 not equal to ten

当程序*未*按 32 位编译时引发的更有意义的断言:

static_assert(sizeof(void *) == 4, 
       "This code should only be compiled as 32-bit.");

因为任何类型指针的大小都与其编译时选择的目标平台大小相同。

对于早期编译器,我们需要使用 _STATIC_ASSERT,它只不过是声明一个*条件大小*的数组。因此,如果条件为真,则声明一个大小为 1 的数组;如果条件为假,则声明一个大小为 0 的数组 - 这会导致编译器错误。该错误不友好。

  • 错误 C2466:无法分配大小为 0 的常量数组

外部参考


Lambda 表达式

这是 C++ 添加的最引人注目的语言特性之一。它有用、有趣,而且也很复杂!我将从绝对基本语法和示例开始,以使其清晰。因此,下面前几个代码示例可能不体现 lambda 的*有用性*。但请放心,lambda 是 C++ 语言中一个非常强大但又代码简洁的特性!

在我们开始之前,请让我先简要介绍一下

  • Lambda*类似于*局部定义的函数。您几乎可以在任何可以放置正则表达式或调用函数的地方实现 lambda。(回想一下 VC++ 编译器报的*“不允许定义局部函数”*错误?)

绝对基本的 lambda

[]{};  // In some function/code-block, not at global level.

是的,上面的代码是完全有效的(仅在 C++0x 中!)。

[] 是 **Lambda 引入器**,它告诉编译器后面的表达式/代码是 lambda。{} 是 lambda 的定义,就像任何函数/方法一样。上面定义的 lambda 不接受任何参数,不返回值,当然,也不做任何事情。

让我们继续...

[]{ return 3.14159; }; // Returns double

上面编码的 lambda 执行简单的工作:返回 PI 的值。但是谁在调用这个 lambda?返回值去哪里了?让我们继续深入

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

有意义了吗?lambda 的返回值被存储在一个局部变量 pi 中。另外,请注意上面示例中被调用的 lambda(注意末尾的函数调用)。这是一个最小化的程序:

int main()
{
   double pi;
   pi = []{return 3.14159;}(); 
   std::cout << pi;
}

Lambda 末尾的括号实际上是在调用 lambda 函数。这个 lambda 不接受任何参数,但末尾仍需要 () 运算符,以便编译器能够知道*调用*。pi lambda 也可以这样实现:

pi = [](){return 3.14159;}(); // Notice the first parenthesis.

这类似于

pi = [](void){return 3.14159;}(); // Note the 'void'

尽管如此,如果您为无参 lambda 添加第一个括号,或者不添加,这取决于您的选择。但我建议您添加它们。C++ 标准委员会希望使 lambda 尽可能简单,因此他们(可能)为无参 lambda 使 lambda 参数规范可选。

让我们继续,当 lambda 接受一个参数时

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

第一个括号 (int n) 指定了 lambda 的参数规范。第二个括号 (41) 将值传递给 lambda。lambda 的主体测试传递的数字是否能被 2 整除。我们现在可以像这样实现 max 或 min lambda:

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);

在这里,我将声明和赋值变量放在同一行,而不是单独声明和赋值。lambda 现在接受两个参数,并返回其中一个值,该值将被赋给 nMinnMax。同样,lambda 可以接受更多参数,也可以接受多种类型的参数。

您应该有的一些问题

  • 返回值怎么样?只有 int 可用吗?
  • 如果 lambda 不能在一个 return 语句中表达怎么办?
  • 如果 lambda 需要做些什么,比如显示值,执行其他操作怎么办?
  • 我能否存储一个对已定义 lambda 的引用,并在别处重用它?
  • Lambda 能调用另一个 lambda 或函数吗?
  • Lambda 被定义为*局部函数*,它能在函数之间使用吗?
  • Lambda 是否访问它被定义或调用的变量?它能修改这些变量吗?
  • 它支持默认参数吗?
  • 它们与函数指针或函数对象(仿函数)有何不同?

在我一一回答之前,让我向您展示 Lambda 表达式文法,并配以以下说明:

LambdaExpression.JPG

问:返回值怎么样?

您可以在 -> 运算符之后指定返回类型。例如:

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

请记住,如插图所示,如果 lambda 只包含一个语句(即,只有一个 return 语句),则无需指定返回类型。所以,在上面的例子中,指定 double 作为返回类型是可选的。是否指定自动推断的返回类型取决于您。

指定返回类型是强制的示例

int nAbs = [] (int n1) -> int 
{
    if(n1<0)
       return -n1;
    else
       return n1;
}(-109);

如果您不指定 -> int,编译器将引发:

error C3499: a lambda that has been specified to have 
             a void return type cannot return a value

如果编译器*只*看不到 return 语句,它将推断 lambda 的返回类型为 void。返回类型可以是任何东西

[]()->int* { }
[]()->std::vector<int>::const_iterator& {}
[](int x) -> decltype(x) { }; // Deducing type of 'x'

它不能返回数组。它也不能将 auto 作为返回类型

[]()-> float[] {};  // error C2090: function returns array
[]()-> auto  {};    // error C3558: 'auto': a lambda return type cannot contain 'auto'

是的,当然,您可以将 lambda 的返回值放入 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);

最后一点,如果您为无参 lambda 指定了返回类型,您*必须*使用括号。以下是错误的:

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

问:如果 lambda 不能在一个 return 语句中表达怎么办?

上面的解释足以说明 lambda 可以包含常规函数可以拥有的任何代码。lambda 可以包含函数/方法可以拥有的所有内容 - 局部变量、静态变量、对其他函数的调用、内存分配,以及**其他 lambda!** 以下代码是有效的(尽管很荒谬!):

[]()
{
   static int stat=99;
   class TestClass 
   { 
      public:
        int member;
   };

   TestClass test; 
   test.member= labs(-100);


   int ptr = [](int n1) -> int*
   {
      int* p = new int;
      *p = n1;
      return p;
   }(test.member);

   delete ptr;
};

问:如果 lambda 需要做些什么,比如显示值,执行其他操作?我能否存储一个对已定义 lambda 的引用,并在别处重用它?Lambda 被定义为*局部函数*,它能在函数之间使用吗?

让我们定义一个确定数字是否为偶数的 lambda。使用 auto 关键字,我们可以将 lambda *存储*在一个变量中。然后我们可以使用该变量(即调用 lambda)!我稍后将讨论*lambda 的类型*是什么。它就像:

auto IsEven = [](int n) -> bool 
{ 
     if(n%2 == 0)
        return true;
     else
        return false; 
};  // No function call!

正如您所能推断的,lambda 的返回类型是 bool,它接受一个参数。重要的是,我们*没有调用 lambda*,只是定义它。如果您加上 (),并带有一些参数,变量的类型将是 bool,而不是*lambda 类型*!现在,局部定义的*函数*(即 lambda)可以在以上语句之后调用:

IsEven(20);

if( ! IsEven(45) )
      std::cout << "45 is not even"; 

上面给出的 IsEven 的定义与调用两次的函数在同一个函数中。如果您希望从其他函数调用 lambda 怎么办?嗯,有几种方法,比如存储在某个局部变量或类级变量中,将其传递给另一个函数(就像函数指针一样),然后从另一个函数调用。另一种机制是存储函数并在全局范围内定义它。由于我还没有讨论*lambda 类型*是什么,我们将稍后使用第一种方法。但让我们讨论第二种方法(全局范围):

示例

// The return type of lambda is bool
// The lambda is being stored in IsEven, with auto-type
auto IsEven = [](int n) -> bool 
{ 
   if(n%2 == 0) return true; 
   else return false; 
}

void AnotherFunction()
{
   // Call it!
   IsEven (10); 
} 

int main()
{
    AnotherFunction();
    IsEven(10); 
}

由于 auto 关键字仅适用于局部或全局范围,我们可以使用它来存储 lambda。我们需要知道*类型*,以便将其存储在类变量中。稍后。

如前所述,lambda 几乎可以做常规函数能做的任何事情。所以,显示值不是 lambda 不能做的事情。它可以显示值。

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);
}

需要注意的一个重要事项是,局部定义的 lambda 不会从它们被定义的上层作用域获取命名空间解析。因此,std 命名空间包含对于 DisplayIfEven 不可用。

问:Lambda 能调用另一个 lambda 或函数吗?

当然。前提是 lambda/函数名在调用时已知,就像函数调用在函数中一样。

问:Lambda 支持默认参数吗?

不支持。

问:Lambda 是否访问它被定义或调用的变量?它能修改这些变量吗?它们与函数指针或函数对象(仿函数)有何不同?

现在我将讨论我一直留空的部分:**捕获规范**。

Lambda 可以是以下之一:

  • 有状态的
  • 无状态的

*状态*定义了如何*捕获*来自更高作用域的变量。我将它们分为以下几类:

  1. 没有变量从上层作用域访问。这是我们到目前为止一直在使用的。
  2. 变量以只读模式访问。您不能修改上层作用域变量。
  3. 变量被*复制*到 lambda(同名),您可以修改副本。这类似于函数调用中使用的*按值调用*机制。
  4. 您对上层作用域变量拥有完全的访问权限,使用相同的名称,并且可以修改该变量。

您应该理解这四类是以下 C++ 咒语的衍生:

  1. 变量是 private 的,您根本无法访问。
  2. const 方法中,您不能修改变量。
  3. 变量*按值传递*给函数/方法。
  4. 变量在方法中完全可访问。或者说,变量是按引用传递的。

让我们玩转捕获!如上图所示,捕获规范在 [] 中给出。以下语法用于指定捕获规范:

  • [] - 不捕获任何内容。
  • [=] - 通过*值*捕获所有内容。
  • [&] - 通过*引用*捕获所有内容。
  • [var] - 通过值捕获 var;不以任何模式捕获其他任何内容。
  • [&var] - 通过引用捕获 var;不以任何模式捕获其他任何内容。

示例 1

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

[a](void)  // Capturing ONLY 'a' by value
{
    std::cout <<  "Value of a="<< 
    a << std::endl;
        
    // cannot modify
    a++;  // error C3491: 'a': a by-value capture 
          //       cannot be modified in a non-mutable lambda


    // Cannot access other variables
    std::cout << b << c;
    // error C3493: 'b' cannot be implicitly captured
    // because no default capture mode has been specified

}();

示例 2

auto Average = [=]() -> float  // '=' says: capture all variables, by value
{
    return ( a + b + c ) / 3.0f;

   // Cannot modify any of the variables
};

float x = Average();

示例 3

// With '&' you specify that all variables be captured by REFERENCE
auto ResetAll = [&]()->void 
{
    // Since it has captured all variables by reference, it can modify them!
    a = b = c = 0;
};

ResetAll();
// Values of a,b,c is set to 0;

指定 = 表示*按值*。指定 & 表示*按引用*。让我们进一步探讨这些。为了简短起见,我现在不将 lambda 放入 auto 变量然后调用它们。取而代之的是,我直接调用它们。

示例 4

// Capture only 'a' and 'b' - by value
int nSum = [a,b] // Did you remember that () is optional for parameterless lambdas!?
{ 
    return a+b;        
}();
 
std::cout << "Sum: " << nSum;

如示例 4 所示,我们可以在* lambda 引入器*([] 运算符)中指定多个捕获规范。再举一个例子,其中三个数(abc)的总和将*存储*在 nSum 变量中。

示例 5

// Capture everything by-value, BUT capture 'nSum' by reference.
[=, &nSum]
{
    nSum = a+b+c;
}();

在上面的示例中,全部按值捕获(即,= 运算符)指定了*默认捕获模式*,而 &nSum 表达式则覆盖了它。请注意,默认捕获模式,它指定全部捕获,必须出现在其他捕获之前。因此,=& 必须出现在其他规范之前。以下会导致错误:

// & or = must appear as first (if specified).
[&nSum,=]{}
[a,b,c,&]{} // Logically same as above, but erroneous.

更多示例

[&, b]{}; // (1) Capture all by reference, but 'b' by value
[=, &b]{}; // (2) Capture all by value, but 'b' by reference
[b,c, &nSum]; // (3) Capture 'b', 'c' by value,
                  // 'nSum' by reference. Do no capture anything else.
[=](int a){} // (4) Capture all by value. Hides 'a' - since a is now
             // function argument. Bad practise. Compiler doesn't warn!
[&, a,c,nSum]{}; // Same as (2).
[b, &a, &c, &nSum]{} // Same as (1)
[=, &]{} // Not valid!
[&nSum, =]{} // Not valid!
[a,b,c, &]{} // Not valid!

正如您所见,有多种组合可以捕获同一组变量。我们可以通过添加来扩展捕获规范语法:

  • [&,var] - 全部按引用捕获,除了 var,按值捕获。
  • [=, &var] - 全部按值捕获,除了 var,按引用捕获。
  • [var1, var2] - 按值捕获 var1, var2
  • [&var1, &var2] - 按引用捕获 var1, var2
  • [var1, &var2] - 按值捕获 var1,按引用捕获 var2

到目前为止,我们已经看到我们可以阻止某些变量被捕获,按值捕获 const,以及按引用捕获 non-const。因此,我们已经涵盖了捕获类别中的 1、2 和 4(见上文)。无法捕获*const 引用*(即 [const &a])。我们现在来看最后一个 - 以*按值调用*模式捕获。

mutable 规范

在参数规范的括号之后,我们指定 mutable 关键字。使用此关键字,我们将所有*按值*捕获的变量置于*按值调用*模式。如果您不加 mutable 关键字,所有按值变量都是常量,您不能在 lambda 中修改它们。添加 mutable 会强制编译器创建被按值捕获的所有变量的副本。然后您可以修改所有按值捕获。没有方法可以*选择性地*捕获 const 和 non-const 按值。或者简单地说,您可以认为它们被作为参数传递给 lambda。

示例

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

[=]()mutable->void // () are required, when you specify 'mutable'
{
    x++;
// Since all variable are captured in call-by-value mode,
// compiler raises warning that y,z are not used!
}();
// the value of x remain zero

在 lambda 调用之后,'x' 的值仍然是零。因为只修改了 x 的*副本*,而不是引用。同样有趣的是,编译器只对 yz 发出警告,而不是对之前定义的(abc...)变量。但是,如果您使用之前定义的变量,它不会抱怨。智能编译器 - 我对此无法多说!

Lambda 与函数指针或函数对象有何不同?

函数指针不维护状态。Lambda 可以。通过按引用捕获,lambda 可以在调用之间维护其*状态*。函数则不能。函数指针不是类型安全的,它们容易出错,我们必须处理调用约定,并且需要复杂的语法。

函数对象可以很好地维护状态。但即使是为一个小型例程,您也必须编写一个类,将其中的一些变量放入其中,并重载 () 运算符。重要的是,您必须在函数块外部完成此操作,以便*其他*函数,该函数将调用该类的 operator(),必须知道它。这会破坏代码流程。

Lambda 的类型是什么?

Lambda 实际上是类。您可以将它们存储在 function 类对象中。该类,对于 lambda,定义在 std::tr1 命名空间中。让我们看一个例子:

#include<functional>
....
std::tr1::function<bool(int)>s IsEven = [](int n)->bool { return n%2 == 0;};
...
IsEven(23);

tr1 命名空间代表技术报告 1,C++0x 委员会成员使用它。请自行搜索更多信息。<bool(int)> 表示 function 类的模板参数,它表示:函数返回 bool 并接受一个 int 参数。取决于将 lambda 放入函数对象中,您必须正确地对其进行类型转换;否则,编译器将发出类型不匹配的错误或警告。但是,正如您所见,使用 auto 关键字要方便得多。

但是,在某些情况下,您必须使用 function - 当您需要将 lambda 传递给函数调用时。例如:

using namespace std::tr1;
void TakeLambda(function<void(int)> lambda)
// Cannot use 'auto' in function argument
{
    // call it!
    lambda(32);
}

// Somewhere in program ... 
TakeLambda(DisplayIfEven); // See code above for 'DisplayIfEven'

DisplayIfEven lambda(或函数!)接受 int,并且不返回任何内容。function 类在 TakeLambda 的参数中以相同的方式使用。此外,它调用 lambda,最终调用 DisplayIfEven lambda。

我已经简化了 TakeLamba,它应该(按增量显示)是:

// Reference, should not copy 'function' object
void TakeLambda(function< void(int) > & lambda);

// Const reference, should not modify function object
void TakeLambda(const function< void(int) > & lambda);

// Fully qualified name
void TakeLambda(const std::tr1::function< void(int) > & lambda);

引入 Lambda 到 C++ 的目的是什么?

Lambda 对于许多 STL 函数非常有用 - 需要*函数指针*或*函数对象*(带有重载的 operator())的函数。简而言之,lambda 对于那些需要*回调函数*的例程很有用。最初,我不会涵盖 STL 函数,但会以更简单易懂的方式解释 lambda 的可用性。非 STL 示例可能显得多余和无意义,但足以阐明这个主题。

例如,以下函数*需要*传递一个函数。它将调用传递的函数。函数指针、函数对象或 lambda 的类型应返回 void 并将 int 作为唯一参数。

void CallbackSomething(int nNumber, function<void(int)> callback_function) 
{
    // Call the specified 'function'
    callback_function(nNumber);
}

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

// Function
void IsEven(int n)
{
   std::cout << ((n%2 == 0) ? "Yes" : "No");
}

// Class with operator () overloaded
class Callback
{
public:
   void operator()(int n)
   {
      if(n<10)
         std::cout << "Less than 10";
      else
         std::cout << "More than 10";
   }
};

int main()
{
   // Passing function pointer
   CallbackSomething(10, IsEven);

   // Passing function-object
   CallbackSomething(23, Callback());

   // Another way..
   Callback obj;
   CallbackSomething(44, obj);

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

好的!现在我希望 Callback 类能够显示一个数字是否大于某个 **N**(而不是固定的 10)。我们可以这样做:

class Callback
{
   /*const*/ int Predicate;
public:
   Callback(int nPredicate) : Predicate(nPredicate) {}

   void operator()(int n)
   {
      if( n < Predicate)
         std::cout << "Less than " << Predicate;
      else
         std::cout << "More than " << Predicate;
   }
};

为了使这可以调用,我们只需要用一个整数常量来构造它。原始的 CallbackSomething*不需要*更改 - 它仍然可以调用带有整数参数的例程!我们这样做到:

// Passing function-object
CallbackSomething(23, Callback(24));
// 24 is argument to Callback CTOR, not to CallbackSomething!

// Another way..
Callback obj(99); // Set 99 are predicate
CallbackSomething(44, obj);

这样,我们就使 Callback 类能够维护其*状态*。请记住,只要对象存在,它的状态就存在。因此,如果您将 obj 对象传递给多次 CallbackSomething(或其他类似函数)的调用,它将具有相同的*谓词*(状态)。正如您所知,这与函数指针*不*可能做到 - 除非我们给函数添加另一个参数。但这样做会破坏整个程序结构。如果某个函数*要求*一个具有特定类型的可调用函数,我们就必须只传递该类型的函数。函数指针无法维护状态,因此在这种情况下不可用。

**Lambda 能做到吗?** 如前所述,lambda 可以通过捕获规范来维护状态。因此,*是的*,通过 lambda 实现这种*有状态*的功能是可能的。这是修改后的 lambda,存储在一个 auto 变量中:

int Predicate = 40;

// Lambda being stored in 'stateful' variable
auto stateful  = [Predicate](int n)
   {  if( n < Predicate)
           std::cout << "Less than " << Predicate;
         else
           std::cout << "More than " << Predicate; 
   };

CallbackSomething(59, stateful ); // More than  40
    
Predicate=1000; 
CallbackSomething(100, stateful);  // Predicate NOT changed for lambda!

stateful lambda 在函数中局部定义,比函数对象更简洁,比函数指针更干净。而且,它现在有了它的*状态*。因此,它将打印“大于 40”表示第一次调用,以及*第二次调用*也是如此。

请注意,Predicate 按值传递(非可变),因此修改原始变量*不会*影响其在 lambda 中的状态。要反映 lambda 中的*谓词修改*,我们只需按引用捕获此变量。当我们像这样修改 lambda 时,第二次调用将打印“小于 1000”。

auto stateful  = [&Predicate](int n) // Capturing by Reference 

这类似于在类中添加一个像 SetPredicate 这样的方法,该方法会修改谓词(状态)。请参阅 VC++ 博客,下面有链接,了解 lambda - 类映射的讨论(博主称之为*心理翻译*)。

与 STL 一起使用

for_each STL 函数对范围/集合中的每个元素调用指定的函数。由于它使用模板,因此它可以接受任何类型的数据类型作为其参数。我们将以此为例使用 lambda。为简单起见,我将使用普通数组,而不是向量或列表。例如:

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 函数内的 lambda。它调用两个函数各 10 次,因为范围包含/指定了 10 个元素。我无需重复说 for_each 的第二个参数*完全相同*(哦!但我重复了!)。

这是一个非常简单的例子,其中 for_each 和 lambda 可以用来显示值*而无需*编写函数或类。当然,lambda 可以进一步扩展以执行额外的工作 - 例如,显示一个数字是素数还是非素数,或计算总和(使用按引用捕获),或修改(例如,乘以 4)范围中的元素。

修改 lambda 参数?

是的!您可以这样做。我一直谈论按引用捕获并进行修改,但没有涵盖修改参数本身。直到现在还没有出现这种需求。为此,只需按引用(或指针)获取 lambda 的参数:

// The 'n' is taken as reference (NOT same as capture-by-reference!)
for_each(Array, Array+10, [](int& n){ n *= 4; });

上面的 for_each 调用将 Array 的每个元素乘以 4。

就像我解释了如何使用 for_each 函数一样利用 lambda,您也可以将其用于其他 algorithm 函数,如 transformgenerateremove_if 等。Lambda 不仅限于 STL 算法,它们也可以高效地用于任何需要函数对象的地方。您需要确保它接受正确的参数数量和类型,并检查它是否需要参数修改等。由于本文档不是关于 STL 或模板的,因此我将不再进一步讨论。

Lambda 不能用作函数指针

是的,相当令人失望和困惑,但却是真的!您*不能*将 lambda 用作需要**函数指针**的函数的参数。通过示例代码,让我先说明我的意思:

// Typedef: Function pointer that takes and int
typedef void (*DISPLAY_ROUTINE)(int);

// The function, that takes a function-pointer
void CalculateSum(int a,int b, DISPLAY_ROUTINE pfDisplayRoutine)
{
   // Calling the supplied function pointer
   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'

**为什么?** 因为 lambda 是面向对象的,它们实际上是类。编译器在内部为 lambda 生成类模型。该*内部*生成的类重载了 operator ();并且具有一些数据成员(通过*捕获规范*和*mutable 规范*推断)- 这些可能是 const、引用或普通成员变量,以及***经典*** 的东西。该类*不能*被*降级*为普通函数指针。

之前的示例是如何运行的?

嗯,这是因为一个名为 std::function 的智能类!请看(上文)CallbackSomething 实际上接受 function 作为参数,而不是*函数指针*。

for_each 一样 - 这个函数不接受 std::function,而是使用模板。它直接用括号调用传递的参数。仔细理解*简化*实现:

template <class Iteartor, class Function>
void for_each(Iteartor first, Iterator, Function func)
// Ignore return type,and other arguments
{ 
  // Assume the following call is in loop
  // The 'func' can be normal function, or 
  // it can be a class object, having () operator overloaded.

  func(first);
}

同样,其他 STL 函数,如 findcount_if 等,将适用于*所有三种*情况:函数指针、函数对象和 lambda。

因此,如果您计划在 SetTimerEnumFontFamilies 等 API 中使用 lambda - 请取消计划!即使强制类型转换 lambda(通过获取其地址),它也行不通。程序将在运行时崩溃。

外部参考


尾随返回类型

让我们从一个简单的例子开始。以下是一个返回类型为 long 的函数。它*不是* lambda,而是一个函数。

auto GetCPUSpeedInHertz() -> long
{
    return 1234567890;
}

如您所见,它返回一个 long。它使用了指定返回类型的新语法,称为**尾随返回类型**。左侧的 auto 关键字只是一个占位符,实际类型在 -> 运算符之后指定。再举一个例子:

auto GetPI() -> decltype(3.14)
{
    return 3.14159;
}

其中返回类型是从表达式中*推导*出来的。请重新阅读上面的 decltype 关键字说明,以便回忆。对于上面给出的两个函数,您显然*不必*使用此功能!

实际应该在哪里使用?

考虑模板函数:

template <typename FirstType, typename SecondType>
/*UnknonwnReturnType*/  AddThem(FirstType t1, SecondType t2)
{
    return t1 + t2;
}

该函数将两个任意值相加,并返回结果。现在,如果我将 intdouble 分别作为第一个和第二个参数传递,返回类型应该是什么?您会说是 double。这是否意味着我们应该将 SecondType 作为函数的返回类型?

template <typename FirstType, typename SecondType>
SecondType AddThem(FirstType t1, SecondType t2);

实际上,我们不能。原因很明显,因为该函数可能与任何类型的左侧或右侧参数一起调用,并且任一类型都可能具有更高的精度。例如:

AddThem(10.0, 'A');
AddThem("CodeProject", ".com");
AddThem("C++", 'X');
AddThem(vector_object, list_object);

此外,函数中的 + 运算符*可能*会调用另一个重载函数,该函数可能返回第三种类型。解决方案是使用以下方法:

template <typename FirstType, typename SecondType>
auto  AddThem(FirstType t1, SecondType t2) -> decltype(t1 + t2)
{
    return t1 + t2;
}

decltype 的解释中所述,该类型是通过表达式确定的;t1+t2 的实际类型以此方式确定。如果编译器可以升级类型,它会这样做(例如,intdouble 升级为 double)。如果类型是类,并且 + 调用了重载运算符,则该重载 + 运算符的返回类型将是返回类型。如果类型不是原生类型,并且找不到重载,编译器将发出错误。重要的是要注意,类型推导仅在您*实例化*模板函数并提供某些数据类型时发生。在此之前,编译器不会进行任何检查(与普通模板函数相同的规则)。


R 值引用

我假设您知道按值传递、按引用传递和按常量引用传递的含义。我进一步假设您知道 L 值和 R 值是什么意思。现在,让我们看一个 R 值引用有意义的例子:

class Simple {};

Simple GetSimple()
{
    return Simple();
}

void SetSimple(const Simple&)
{ }

int main()
{
    SetSimple( GetSimple() );
}

在这里,您可以看到 GetSimple 方法返回一个 Simple 对象。并且,SetSimple 按引用接受一个 Simple 对象。在调用 SetSimple 时,我们正在传递 GetSimple - 正如您所见,返回的对象即将被销毁,一旦 SetSimple 返回。让我们扩展 SetSimple

void SetSimple(const Simple& rSimple)
{
   // Create a Simple object from Simple.
   Simple object (rSimple);
   // Use object...
}

请忽略缺失的复制构造函数,我们使用的是默认复制构造函数。为了理解,假设复制构造函数(或普通构造函数)正在分配一定量的内存,比如 100 字节。析构函数应该销毁 100 字节。在这里没看到问题?好的,让我解释一下。正在创建两个对象(一个在 GetSimple 中,一个在 SetSimple 中),它们都分配了 100 字节内存。对吗?这就像复制一个文件/文件夹到另一个位置。但正如您从示例代码中看到的,只有“object”被使用。那么,为什么我们要两次分配 100 字节?为什么我们不能使用第一个 Simple 对象构造分配的 100 字节?在早期版本的 C++ 中,没有简单的方法,除非我们编写自己的内存/对象管理例程(如 MFC/ATL 的 CString 类)。在 C++0x 中,我们可以做到。因此,简而言之,我们将通过以下方式优化例程:

  1. 创建第一个对象,并分配内存。
  2. 它*即将*被销毁。
  3. 在销毁之前,我们将第一个对象的内存附加到第二个对象。
  4. 我们从第一个对象中分离内存(例如,将指针设置为 null)。
  5. 我们使用第二个对象。
  6. 第二个对象被销毁,最终释放了第一个对象最初分配的内存。

通过这种方式,我们节省了 100 字节!虽然金额不大。但如果此特性用于字符串、向量、列表等更大的容器中,它将节省大量的内存和时间!因此,它将提高整体应用程序性能。尽管问题和解决方案以 RVO 和 NRVO(命名返回值优化)的形式提供,但在 Visual C++ 2005 及更高版本中,它并不像效率高且有意义。为此,我们使用了一个新引入的运算符:**R 值引用声明符:&&**。基本语法:Type**&&** identifier。现在我们一步一步地修改上面的代码:

void SetSimple(Simple&& rSimple) // R-Value NON-CONST reference
{
   // Performing memory assignment here, for simplicity.
   Simple object;
   object.Memory = rSimple.Memory;
   rSimple.Memory = nullptr; 

 
   // Use object...
   delete []object.Memory;
}

上面的代码用于将内容*移动*从旧对象到新对象。这非常类似于移动文件/文件夹。请注意,const 已被移除,因为我们还需要重置(分离)第一个对象最初分配的内存。类和 GetSimple 按如下方式修改。该类现在有一个内存指针(例如,void*)名为 Memory。默认构造函数将其设置为 null。此成员被设为 public 以简化主题。

class Simple
{
public:
    void* Memory;
    Simple() { Memory = nullptr; }
    Simple(int nBytes) { Memory = new char[nBytes]; }
};

Simple GetSimple()
{
    Simple sObj(10);

    return sObj;
}

如果您进行如下调用怎么办:

Simple x; 
SetSimple(x);

这将导致错误,因为编译器无法将 Simple 转换为 Simple&&。变量 'x' 不是临时的,不能表现得像 R 值引用。为此,我们可以提供一个重载的 SetSimple 函数,该函数接受 SimpleSimple&const Simple&。因此,您现在知道临时对象实际上是 R 值引用。使用 R 值,您可以实现所谓的**移动语义**。移动语义使您能够编写转移资源(如动态分配的内存)从一个对象到另一个对象的代码。为了实现移动语义,我们需要为该类提供一个移动构造函数,以及可选的移动赋值运算符(operator =)。

移动构造函数

让我们在类内部通过移动构造函数来实现*移动对象*。如您所知,复制构造函数的签名如下:

Simple(const Simple&);

移动构造函数将非常相似 - 只多一个 ampersand:

Simple(Simple&&);

但正如您所见,移动构造函数是**非 const** 的。就像复制构造函数*可以*接受一个非 const 对象(从而修改源!),移动构造函数也可以接受const;没有什么能阻止这一点 - 但这样做会放弃编写移动构造函数的全部目的。为什么?如果您正确理解了,我们正在从源(移动构造函数的参数)分离*资源所有权*。在我写更多可能让您困惑的文字之前,让我们看一个所谓的移动构造函数*可能*被调用的例子:

Simple GetSimple()
{
   Simple sObj(10);
   return sObj;
}

为什么?对象 sObj 是在堆栈上创建的。返回类型是 Simple,这意味着将调用复制构造函数(如果提供;否则,将调用编译器提供的默认)。此外,将调用 sObj 的析构函数。现在,假设移动构造函数可用。在这种情况下,编译器知道对象正在被*移动*(所有权转移),它将调用移动构造函数而不是复制构造函数。

  • 与复制构造函数不同,编译器*不*提供默认移动构造函数;您必须自己编写。

这是更新后的 Simple 类实现:

class Simple
{
    // The resource
    void* Memory;

public:
    
    Simple() { Memory = nullptr; }

    // The MOVE-CONSTRUCTOR
    Simple(Simple&& sObj)
    {
                  // Take ownership
        Memory = sObj.Memory;
 
                  // Detach ownership
        sObj.Memory = nullptr;
    }

    Simple(int nBytes)     
    {        
        Memory = new char[nBytes];     
    }

    ~Simple()
    {
        if(Memory != nullptr)
            delete []Memory;
    }
};

以下是调用 GetSimple 函数时发生的情况:

  1. 程序控制进入 GetSimple 函数,在堆栈上分配一个新的 Simple 对象。
  2. 调用类 Simple 的(创建)构造函数。
  3. 构造函数分配所需的字节数(*资源*)。
  4. return 语句已准备好将堆栈对象 sObj 转换为可返回的对象。
  5. 在这里,智能编译器发现对象实际上正在被移动;并发现移动构造函数可用,它调用 MC。
  6. 移动构造函数(Simple(Simple&&))现在获取 Memory 内容(而不是像复制构造函数那样再次分配)。然后它将原始对象的 Memory 设置为 null。它不分配和复制内存!
  7. 控制返回到*返回点*。现在原始的 sObj 将被销毁。
  8. 调用 sObj 的析构函数(~Simple()),该析构函数看到 Memorynull - 什么也不做!

注意并清楚地理解这一点非常重要:

  • MC 被调用*仅仅*因为它可用;否则,将调用 CC(默认或用户定义的)。
  • 按值返回对象(即,Simple,*不是* Simple&Simple*)会导致调用复制构造函数或移动构造函数。这一点非常重要,需要理解!
  • 移动构造函数通过了解析构函数实际做什么来分离对象。在这种情况下,我们将 Memory 设置为 null,这样 DTOR 就不会删除它。
  • 检查指针是否为 null(Memory!=nullptr)根据 C++ 标准不是必需的,但为清楚起见在此提及。在您的类中,您必须设计 MC 和 DTOR,使它们具有相似的协议。

我们看到我们已将原始数据移动到新对象。这样,我们就节省了内存和处理时间。如前所述,这种节省微不足道 - 但当它应用于更大的数据结构,以及/或当临时对象被大量创建和销毁时,节省是至关重要的!

移动赋值运算符

理解以下代码:

Simple obj1(40);
Simple obj2, obj3;
 
obj2 = obj1;
obj3 = GetSimple();

如您所知,obj2 = obj1 将调用*赋值运算符*,没有所有权转移。obj2 的内容被 obj1 的内容替换。如果我们不提供赋值运算符,编译器将提供默认赋值运算符(并将逐字节复制)。右侧的对象保持不变。

  • 编译器*不*提供默认移动赋值运算符,而默认(*复制*)赋值运算符则提供。

用户定义的运算符的签名*可以*是:

// void return is put for simplicity
void operator=(const Simple&); // Doesn't modify source!

那么 obj3 = GetSimple() 语句呢?GetSimple 返回的对象是临时的,正如您现在*应该清楚地*知道的。因此,我们可以(也应该)利用所谓的*移动语义*(我们在移动构造函数中也使用了相同的概念!)。这是一个简化的移动赋值运算符:

void operator=(Simple&&); // Modifies the source, since it is temporary

这是修改后的 Simple 类(前面的代码已省略以节省篇幅)。未处理自我赋值:

class Simple
{
   ...
   void operator = (const Simple& sOther)
   {
       // De-allocate current 
       delete[] Memory;
       
       // Allocate required amount of memory, 
       // and copy memory. 

       // The 'new' and 'memcpy' calls not shown
       // for brevity. Another variable in this
       // class is also required to hold buffersize.
   }

   void operator = (Simple&& sOther)
   {
       // De-allocate current 
       delete[] Memory;    

       // Take other's memory contnent
       Memory = sOther.Memory; 

       // Since we have taken the temporary's resource.
       // Detach it!
       sOther.Memory = nullptr;
   }
};

因此,对于 obj3 = GetSimple() 语句,将发生以下情况:

  1. 调用 GetSimple 函数,该函数返回一个*临时*对象。
  2. 现在,将调用特殊函数*移动赋值运算符*。由于编译器识别出该*特殊函数*的参数是临时的,因此它会调用接受**R 值引用**的赋值运算符。这种情况与上面提到的 SetSimple 相同。
  3. 移动赋值运算符获取所有权,并从所谓的临时对象/R 值引用分离所有权。
  4. 调用*临时对象*的析构函数,该析构函数识别出对象不拥有任何资源 - 它什么也不做。

就像移动构造函数一样,移动赋值运算符和析构函数(简而言之,全部三个)必须就*资源*分配/释放遵守相同的协议。

另一个例子(级联)

假设我们一直在使用的 Simple 类是一个数据容器;如字符串、日期/时间、数组,或您喜欢的任何内容。该数据容器旨在通过运算符重载允许以下操作:

Simple obj1(10), obj2(20), obj3, obj4; // Pardon me for using such names!

obj3 = obj1 + obj2;
obj2 = GetSimple() + obj1;
obj4 = obj2 + obj1 + obj3;

您只需说:“*在该类中提供加号 (+) 运算符*”。好的,我们在 Simple 类中提供一个 operator+

// This method is INSIDE the class
Simple operator+(const Simple&)
{
   Simple sObj;
   // Do binary + operation here...
   return sObj;
}

现在,正如您所见,创建了一个临时对象并从 operator+ 返回,最终*会导致*调用移动赋值运算符(对于 obj3 = obj1 + obj2 表达式),并且资源得到保存 - 很好!(我希望您在继续阅读下一段之前完全理解它。)对于下一条语句(obj2 = GetSimple() + obj1),*左侧*的对象本身就是临时的。请注意,在 SetSimple 和移动*特殊函数*中,参数是临时的,但在这里不是。据我所知,没有技术可以将此对象设为临时对象。好吧,好吧,我不是 Bjarne Stroustrup;这是解决方案:

class Simple
{
   ...
   // Give access to helper function
   friend Simple operator+(Simple&& left, const Simple& right);
};

// Left argument is temporary/r-value reference
Simple operator+(Simple&& left, const Simple& right)
{
   // Simulated operator+
   // Just shows the resource ownership/detachment
   // Does not use 'right'
   Simple newObj;
   newObj.Memory = left.Memory;
   left.Memory = nullptr;

   return newObj;
}

关于代码的解释

  • operator+ 的非类版本将**R 值**引用作为其左参数;右侧参数是对象的普通*常量引用*。
  • 该函数/重载运算符只是将所有权附加到新对象,并从*'左侧'*(临时对象)分离所有权。由于这只是一个模拟版本,*'右侧'*未使用。然而,在实际的*容器类*中,您会这样做。
  • 返回一个 Simple 对象,该对象刚刚从临时对象那里抢走了所有权。

最后一个语句(obj4 = obj2 + obj1 + obj3)怎么样?

  • 首先,调用 operator+ 的普通类版本(对于 obj2 + obj1)。它返回一个新的 Simple 对象,我们称之为 t1
  • 现在,使用 t1obj2+obj1 的结果),它是一个**临时对象**,再次调用 operator+t1+obj3)- 调用类外版本 operator+,它从 t1 获取所有权。
  • 全局 operator+ 返回另一个(可能*是二元加法*)对象。我们称可返回对象为 t2
  • 现在,t2 要赋值给 obj4,并且由于它也是一个临时对象,将调用*移动赋值运算符*。

以下是更简化的非口头形式(斜体是*正在进行的调用*):

  • obj4 = obj2 + obj1 + obj3
  • obj4 = t1 + obj3
  • obj4 = t2

外部参考


其他语言特性

本节列出了未被添加到 C++0x 标准,但已在 VC8/VC9 编译器中添加的 C++ 特性。它们现在是 C++0x 标准的一部分。

1. 强类型枚举

枚举的 sizeof 是多少?四字节?嗯,取决于您选择的编译器,大小可能会有所不同。枚举的 sizeof 重要吗?是的,如果您将枚举作为类/结构体的成员。sizeof 类/结构体的大小会改变,这使得代码的移植性变差。此外,如果结构体要存储在文件中或传输,问题会进一步加剧。强类型枚举强制类型,从而避免任何 bug 的产生。它们使软件系统内的代码更具可移植性。解决方案是指定枚举的*基类型*:

enum Priority : BYTE // unsigned char
{
    VeryLow = 0,
    Low,
    Medium,
    High,
    VeryHigh
};

这会导致 sizeof(Priority) 为 1 字节。同样,您可以将任何整数类型作为枚举的基类型:

enum ByteUnit : unsigned __int64
{
    Byte = 1,
    KiloByte = 1024,
    MegaByte = 1024L * 1024,
    GigaByte = (unsigned __int64)1 << 30,
    TeraByte = (unsigned __int64)1 << 40,
    PetaByte = (unsigned __int64)1 << 50
};

此枚举的 sizeof 变为 8 字节,因为基类型是无符号 __int64。如果您不指定基类型,在此情况下,编译器将警告您在枚举中放入了超出范围的值。**注意**:Microsoft C/C++ 编译器*仅*部分实现此功能。

**外部参考**:提案 N2347

2. 右尖括号

当您声明模板的模板时,如下例所示,您需要在连续的右尖括号(*大于号*)之间添加额外的空格:

vector<list<int> > ListVector;

否则,编译器将发出错误,因为 >> 是有效的 C++ 标记(*按位右移*)。除了多模板之外,当您使用 static_cast 运算符强制转换为模板时,也可能出现此运算符:

static_cast<vector<int>>(expression);

使用新的 C++0x 标准,您可以使用连续的右尖括号(甚至多次),例如:

vector <vector<vector<int>>> ComplexVector; 

外部参考:提案 N1757

3. Extern 模板

我未能找到 extern 模板的确切目的和含义。无论如何,我将分享我通过这个术语发现的内容。确实,我可能错了 - 并期望您分享您的知识,以便我更新这一部分。我推断的是:

  1. 同一个*类型*的模板的不同*实例化*会导致目标代码重复。使用 extern 模板,我们可以只让一个翻译单元生成相应的代码。
  2. 您可以提前声明*模板实例化*,而不是等到实际在代码中实例化它。这样,模板类以及指定的类型就可以*提前*得到*验证*。

第一点的清晰解释超出了我的理解范围 - 可用的细节模糊且重叠,因此我不讨论第一点。例如,您有一个类:

template <typename T>
class Number
{
   T data;
public:
Number() { data = 0; }
   Number(T t) // or (const T& t)
   { data = t; }
 
   T Add(T t) { return data + t; }
 
   T Randomize(T t) { return data % t; }
};

现在,在您实际为某个数据类型实例化模板之前,您想确保该类对于该数据类型能够编译。也就是说,对于这个例子,类型应该支持 +% 运算符。因此,您可以提前指定模板实例化:

template Number<int>;
template Number<float>;

这会为第二个规范引发错误,因为 float 的运算符 % 无效。同样,当您指定一个不支持模板类可能需要的操作的模板参数时,编译器会抱怨。对于这个模板类,模板类型必须支持赋值为零、赋值运算符、+ 运算符和 % 运算符。请注意,我们实际上并没有实例化模板类。这就像声明一个函数,并指定参数和返回类型。该函数*外部*定义在别处。

**外部参考**:提案 N1987


C++0x 功能的外部参考


文章后续


结论

尽管本文档几乎已完成,但可能仍存在一些小错误、拼写/语法错误、代码小错误等。请告知我。对于**可下载的代码** - 我想知道此内容是否需要一些代码?

历史

  • 首次草稿发布:2010 年 4 月 18 日
  • 第一次修订:2010 年 4 月 19 日
  • 第二次修订:2010 年 4 月 20 日(移动赋值,operator+
  • 第三次修订:2010 年 4 月 21 日(添加了实际的 auto 用法,尾随返回类型解释)
  • 第四次修订:2010 年 4 月 25 日(外部参考,其他功能等)
  • 第五次修订:2010 年 5 月 2 日(解释了 lambda 的目的)
  • 第六次修订:2010 年 5 月 9 日(lambda 不能用作函数指针)
  • 第七次修订:2010 年 5 月 16 日(附加源代码,少量内容更新)
  • 第八次修订:2010 年 10 月 30 日(少量更改,链接到后续文章)
© . All rights reserved.