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






4.94/5 (152投票s)
通过清晰、准确、详细的讨论,阐述新的 C++ 语言特性。
引言
您可能已经知道,C++ 语言正在通过 ISO 标准进行更新。新 C++ 语言的代号是 **C++0x**,许多编译器已经引入了其中的一些特性。本教程旨在为您介绍 C++ 语言的新变化。请注意,我仅为 **Visual C++ 2010** 编译器解释新特性,尽管它们也适用于其他编译器。我不会评论其他编译器的绝对语法。
本文档假设您对 C++ 有中等水平的知识,并且您知道什么是类型转换、什么是 const 方法以及什么是模板(基本意义上)。
C++ 新特性
以下是我将要讨论的新 C++ 语言特性的列表。我对 lambda 和 R 值给予了更多关注,因为我觉得找不到任何易于理解的内容。目前,为了简单起见,我没有使用模板或 STL,但我可能会更新本文档以 *添加* 关于模板/STL 的内容。
auto 关键字 |
用于在编译时根据赋值自动推导数据类型。 |
decltype 关键字 |
用于从表达式或 auto 变量推导数据类型。 |
nullptr 关键字 |
空指针现在得到了提升,并拥有了自己的关键字! |
用于编译时断言。对于模板和无法通过 #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*
使用 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};
// 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
参数,您只需使用模板!;)
您不能在 class
或 struct
中拥有 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
,然后您修改函数返回类型以返回 short
或 double
,原始的 *自动* 变量定义将失效。如果您*运气不好*,并且只在一个 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
iterator
与 const_iterator
不同;由于我没有写关于 STL 的内容,请自行阅读/实验)。
为了克服所有这些复杂性,并帮助 auto
关键字,标准 C++ 现在为 STL 容器提供了 cbegin
、cend
、crbegin
和 crend
方法。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 之后讨论这一点。
外部参考
- auto 关键字(类型推导) - MSDN
'decltype' 关键字
这个 C++ 运算符给出表达式的类型。例如
int nVariable1;
...
decltype(nVariable1) nVariable2;
声明 nVariable2
为 int
类型。编译器知道 nVariable1
的类型,并将 decltype(nVariable1)
翻译为 int
。decltype
关键字与 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
成员,最后是它的分配器类型。因此,*推导出的类型*对于 string
是 std::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();
外部参考
- decltype 类型说明符 - MSDN
'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
是一个关键字,而不是一个类型。因此,您不能对其使用 sizeof
或 decltype
运算符。话虽如此,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 的常量数组
外部参考
- static_assert 关键字 - MSDN
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 现在接受两个参数,并返回其中一个值,该值将被赋给 nMin
和 nMax
。同样,lambda 可以接受更多参数,也可以接受多种类型的参数。
您应该有的一些问题
- 返回值怎么样?只有
int
可用吗? - 如果 lambda 不能在一个 return 语句中表达怎么办?
- 如果 lambda 需要做些什么,比如显示值,执行其他操作怎么办?
- 我能否存储一个对已定义 lambda 的引用,并在别处重用它?
- Lambda 能调用另一个 lambda 或函数吗?
- Lambda 被定义为*局部函数*,它能在函数之间使用吗?
- Lambda 是否访问它被定义或调用的变量?它能修改这些变量吗?
- 它支持默认参数吗?
- 它们与函数指针或函数对象(仿函数)有何不同?
在我一一回答之前,让我向您展示 Lambda 表达式文法,并配以以下说明:
问:返回值怎么样?
您可以在 ->
运算符之后指定返回类型。例如:
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 可以是以下之一:
- 有状态的
- 无状态的
*状态*定义了如何*捕获*来自更高作用域的变量。我将它们分为以下几类:
- 没有变量从上层作用域访问。这是我们到目前为止一直在使用的。
- 变量以只读模式访问。您不能修改上层作用域变量。
- 变量被*复制*到 lambda(同名),您可以修改副本。这类似于函数调用中使用的*按值调用*机制。
- 您对上层作用域变量拥有完全的访问权限,使用相同的名称,并且可以修改该变量。
您应该理解这四类是以下 C++ 咒语的衍生:
- 变量是
private
的,您根本无法访问。 - 在
const
方法中,您不能修改变量。 - 变量*按值传递*给函数/方法。
- 变量在方法中完全可访问。或者说,变量是按引用传递的。
让我们玩转捕获!如上图所示,捕获规范在 []
中给出。以下语法用于指定捕获规范:
[]
- 不捕获任何内容。[=]
- 通过*值*捕获所有内容。[&]
- 通过*引用*捕获所有内容。[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 引入器*([]
运算符)中指定多个捕获规范。再举一个例子,其中三个数(a
、b
、c
)的总和将*存储*在 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 的*副本*,而不是引用。同样有趣的是,编译器只对 y
和 z
发出警告,而不是对之前定义的(a
、b
、c
...)变量。但是,如果您使用之前定义的变量,它不会抱怨。智能编译器 - 我对此无法多说!
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
函数,如 transform
、generate
、remove_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 函数,如 find
、count_if
等,将适用于*所有三种*情况:函数指针、函数对象和 lambda。
因此,如果您计划在 SetTimer
、EnumFontFamilies
等 API 中使用 lambda - 请取消计划!即使强制类型转换 lambda(通过获取其地址),它也行不通。程序将在运行时崩溃。
外部参考
- C++ 中的 Lambda 表达式 in MSDN
- VC++ 团队关于 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;
}
该函数将两个任意值相加,并返回结果。现在,如果我将 int
和 double
分别作为第一个和第二个参数传递,返回类型应该是什么?您会说是 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
的实际类型以此方式确定。如果编译器可以升级类型,它会这样做(例如,int
、double
升级为 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 中,我们可以做到。因此,简而言之,我们将通过以下方式优化例程:
- 创建第一个对象,并分配内存。
- 它*即将*被销毁。
- 在销毁之前,我们将第一个对象的内存附加到第二个对象。
- 我们从第一个对象中分离内存(例如,将指针设置为
null
)。 - 我们使用第二个对象。
- 第二个对象被销毁,最终释放了第一个对象最初分配的内存。
通过这种方式,我们节省了 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
函数,该函数接受 Simple
、Simple&
或 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
函数时发生的情况:
- 程序控制进入
GetSimple
函数,在堆栈上分配一个新的Simple
对象。 - 调用类
Simple
的(创建)构造函数。 - 构造函数分配所需的字节数(*资源*)。
- return 语句已准备好将堆栈对象
sObj
转换为可返回的对象。 - 在这里,智能编译器发现对象实际上正在被移动;并发现移动构造函数可用,它调用 MC。
- 移动构造函数(
Simple(Simple&&)
)现在获取Memory
内容(而不是像复制构造函数那样再次分配)。然后它将原始对象的Memory
设置为null
。它不分配和复制内存! - 控制返回到*返回点*。现在原始的
sObj
将被销毁。 - 调用
sObj
的析构函数(~Simple()
),该析构函数看到Memory
为null
- 什么也不做!
注意并清楚地理解这一点非常重要:
- 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()
语句,将发生以下情况:
- 调用
GetSimple
函数,该函数返回一个*临时*对象。 - 现在,将调用特殊函数*移动赋值运算符*。由于编译器识别出该*特殊函数*的参数是临时的,因此它会调用接受**R 值引用**的赋值运算符。这种情况与上面提到的
SetSimple
相同。 - 移动赋值运算符获取所有权,并从所谓的临时对象/R 值引用分离所有权。
- 调用*临时对象*的析构函数,该析构函数识别出对象不拥有任何资源 - 它什么也不做。
就像移动构造函数一样,移动赋值运算符和析构函数(简而言之,全部三个)必须就*资源*分配/释放遵守相同的协议。
另一个例子(级联)
假设我们一直在使用的 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
。 - 现在,使用
t1
(obj2+obj1
的结果),它是一个**临时对象**,再次调用operator+
(t1+obj3
)- 调用类外版本operator+
,它从t1
获取所有权。 - 全局
operator+
返回另一个(可能*是二元加法*)对象。我们称可返回对象为t2
。 - 现在,
t2
要赋值给obj4
,并且由于它也是一个临时对象,将调用*移动赋值运算符*。
以下是更简化的非口头形式(斜体是*正在进行的调用*):
- obj4 = obj2 + obj1 + obj3
- obj4 = t1 + obj3
- obj4 = t2
外部参考
- R 值引用声明符 - MSDN
- 如何编写移动构造函数 - MSDN
其他语言特性
本节列出了未被添加到 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 模板的确切目的和含义。无论如何,我将分享我通过这个术语发现的内容。确实,我可能错了 - 并期望您分享您的知识,以便我更新这一部分。我推断的是:
- 同一个*类型*的模板的不同*实例化*会导致目标代码重复。使用 extern 模板,我们可以只让一个翻译单元生成相应的代码。
- 您可以提前声明*模板实例化*,而不是等到实际在代码中实例化它。这样,模板类以及指定的类型就可以*提前*得到*验证*。
第一点的清晰解释超出了我的理解范围 - 可用的细节模糊且重叠,因此我不讨论第一点。例如,您有一个类:
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 功能的外部参考
- C++0x on Wikipedia
- Visual C++ 团队关于 C++0x 新功能的博客
- Visual C++ 2010 中的新功能
- GCC 4.5 中的 C++0x 支持
- C++ 技术报告 1
文章后续
结论
尽管本文档几乎已完成,但可能仍存在一些小错误、拼写/语法错误、代码小错误等。请告知我。对于**可下载的代码** - 我想知道此内容是否需要一些代码?
历史
- 首次草稿发布: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 日(少量更改,链接到后续文章)