C++ 11 新增的右值引用 && 及其使用原因






4.61/5 (30投票s)
通过简单的运算符重载提升旧代码的性能
引言
本文旨在解释 C++ 11 标准中引入的新的 && 引用,该标准已包含在最新版本的编译器中,例如 Visual Studio 10-11-12 和 gcc 4.3-4,以及比 gcc 更优秀、更快速(甚至可能更快)的开源替代品 Clang。
为什么要开始使用它? 简而言之:可以获得显著的性能提升。
例如,向 std::vector (或任何数组创建)插入元素将不再需要大量的内存分配/复制操作。
但在深入 C++ 11 的“移动语义”细节之前,我们需要理解问题的核心。我们需要理解为什么 C++ 语言中的赋值 (=) 操作经常导致无用的内存分配/复制,从而产生性能问题。
我们生活在一个需要处理大量数据的世界里。我们需要以高效的方式存储和处理它们。C 和 C++ 的问题在于我们已经习惯了低效的处理方式。
a = 1 // we assigned (we could say copied) number to variable由于在自然语言中,我们也会考虑将数据“赋值”给数组。
在 C 语言中,我们也继续以相同的方式向数组赋值,这似乎是理所当然且可以预见的。
textures[1] = new Texture(640*480*32,"cube.jpg");
通过 = 进行复制的实际成本很小,因为指针只是数字(包含内存地址)。但对于对象来说,情况就不同了。
性能问题是如何开始的
现在我将尽量简短,但重要的是真正理解 && 运算符为何诞生。
在 C++ 中,引用变得普遍,因为它们比指针更安全,并且更重要的是,新的语言特性,如自动调用构造函数或析构函数(自动清理)或运算符,都只与它们一起工作。
请注意,引用在内部仍然只是指针,但这次它们更安全,并且会自动解引用。就这样。它们是如何变得更安全的?嗯。它们永远不会包含无效数据,因为唯一允许对它们进行的赋值是在声明期间进行的,并且只能是对已存在的静态声明的数据。但每当你将引用传递给函数时,它实际上仍然只是一个内部压入堆栈的指针。
void proc( Texture * texture ) { Save(texture); } // unsafe since invalid pointer can be passed in
void proc( Texture & texture ) { Save(texture); } // safer if Texture don't contain invalid pointer inside.
C++ 希望通过为我们处理例行的指针解引用/引用(并向我们隐藏这种指针的魔力)来使我们的生活更轻松。传递或处理对象而不是指向它们的指针的这种错觉是如此完美,以至于许多人失去了对引用和对象的实际概念。
正是这种歧义产生了不幸的副作用,让我们相信这个
Texture texture[1000]; texture[1] = Texture("sky.jpg"); // we are assigning reference to what ? array of references?
只是这个更安全的选择
Texture * texture[1000]; texture[1] = new Texture("sky.jpg"); // we are assigning pointer to array of pointers
我们只是用更安全的引用取代了到处都有的不安全指针。但更重要的是,这使我们可以自动调用构造函数、析构函数和运算符。
对吗?
错误。C++ 中没有所谓的引用数组。
这次没有后台的指针魔法,就像在函数内部一样。
所以我们实际上创造了一个算法上非常糟糕的决定。
我们创建了一个按值存储对象的数组。不,从声明中删除 * 并不会自动使指针变量成为引用。
从性能角度来看,数组指针确实没有替代方案。 对于包含静态声明结构化数据的大型对象,创建、排序、搜索以及尤其是重新分配 100MB 的值数组时,存在巨大的性能差异。然后,与指向它们的少量字节相比,情况就大不相同了。 这几乎扼杀了在处理器缓存中尽可能多地处理数据的可能性。因此,我们以主内存的速度(数量级较慢)而不是 CPU 缓存(我们现在无用地污染了它,充满了不重要的对象数据)来排序/搜索/重新分配等。对于包含大型动态数据 Thus,它们的情况有所改善,但在赋值给数组时仍然很糟糕。因为那时我们需要分配和复制大量的动态内存以及对象的其余部分。
但我认为提到的重新分配是按值存储对象的 worst 后果。
所以每次你想“算了,把它放到 vector<large-object> 里吧”。
你的内存需求将是实际需求的两倍,并且由于所有分配的内存将在每次重新分配后被无用地移动,你的性能将非常糟糕。有人可能会认为,这就是我们最终为通用方法和懒惰所付出的代价。
但在我看来,这是因为上面提到的 oo 原因(自动构造函数、析构函数、运算符)而存储对象而不是指针所付出的代价。
但回到主题。正如我们所记得的,赋值 = “实际上”是复制数据。这导致了数组的另一个重大性能问题。
如何高效地构建大型对象数组。
假设您想建造一座摩天大楼城市,并且显然(由于规模庞大)您无法浪费任何时间和资源。
现在把城市看作数组的类比。所以您实际要做的是您在城市“内部”“创建”建筑。
- 您在城市内部为建筑分配大片空间。
- 显然,您只需在此空间中建造建筑。
但由于我们习惯于在 C 语言中使用 = 运算符将对象指针存储到数组中。
所以,我们尝试将相同的“创建和赋值”范例用于引用,这是很自然的; 换句话说,我们只是习惯了它,更糟糕的是,每一本 C++ 书都教导我们以这种低效的方式进行操作。
Skyscrapper city[1000];
city[1] = Skyscrapper("Empire State Building");
所以,而不是像从城市类比中学到的那样“在数组内部创建”。
- 您在城市外部分配大片临时空间// = Skyscraper();
- 您在该临时空间中建造了摩天大楼。
- 您在城市内部分配了同样大的空间// city[1].operator = (Skyscraper &in);
- 您使用了大量的卡车、汽油和时间将这座摩天大楼运送到城市内的空间。
C++ 中每个普通对象通过引用赋值而不是指针赋值都会遭受这种痛苦,并且赋值给数组是最糟糕的。 只需查看文章中间的基准测试结果。
这几乎是你周围的大部分代码。继续查看你最近的代码。
它在数组上的表现如此强烈是因为有大量的低效赋值。在我们的基准测试中是 500 次赋值,但如果你在循环中进行 500 次迭代的任何引用赋值,都可以通过移动(如下所述)而不是复制来处理,那么你基本上会遇到同样的问题。
但回到数组。那么 C++ 中有没有有效存储对象到数组的方法呢?(该死……我也习惯了 。我的意思是有效地在数组中创建对象)而不会浪费 CPU 和内存?
正如你在基准测试中所见,对象越大,它就越重要。
是的,有,但它们不直观或明显,而且本质上是 hack。
现在,如果 C++ 允许我们调用专门的构造函数并在数组中已分配的空间(该空间仅由一次高效的分配为所有元素分配)中使用它来创建对象。
那么这将节省我们大多数人通过赋值新对象(=)通常会进行的无数次分配/复制。
Skyscrapper city[1000];
city[1]("empire state building") //not possible in c++ will not compile
尽管如此,还是有一些方法可以做到。
例如,您可以将所有初始化代码移到一个方法中,并在数组元素上显式调用它。
Skyscrapper city[1000]; city[1].create("empire state building");
- 您在城市内部分配大片空间。
- 您在此空间中建造了建筑。
万岁。使用 = 引入的问题消失了。
这意味着现在对象是否具有大型静态声明的数组或结构不再重要。没有复制,就没有问题。
最重要的是,移动大部分无用的空字节所浪费的 CPU 时间消失了。
读取和写入如此大块内存也是积极的。
这几乎会刷新所有 CPU 缓存,从而降低其余应用程序的性能,这种情况也消失了。
这一切都很好,但人们会停止将代码放在构造函数中,而是选择某种标准的创建方法,这种可能性很小。
使用构造函数创建一切是我们已经非常习惯并且喜欢的范例。
就像我们被误导的书本训练的那样,并使用 = 运算符将数据存储到数组中。
尽管如此。
有一个方法可以通过鲜为人知的 operator new 变体来使用构造函数完成此操作。它被称为“placement new”,您可以使用您提供的 this
指针在现有内存上构造对象。
但现在我们进入了一个非常奇怪、令人困惑且有点危险的领域,因为“new”这个词在静态声明的数组中到处飞。这个 new 不分配任何东西。这个 new 只是一个为了调用构造函数而进行的 hack。
为什么危险?一旦您重载了像分配器 New 这样基本的东西,就准备好应对各种麻烦了 http://www.drdobbs.com/article/print?articleID=184401369&dept_url=/cpp/
#include <new>
Skyscrapper city[1000];// we allocate static array with all objectsall just once
new (&city[1]) Skyscraper("empire state building"); //no object alloc just calls constructor
city[1].~Skyscraper(); // frees resources allocated by previous constructor call
new (&city[1]) Skyscraper("renovated empire state building");
city[1].~Skyscraper(); // frees resources allocated by previous constructor call
它不自然,这次有问题,而且非常令人困惑。 但存储对象始终是个坏主意,正如您在下面的基准测试中的排序结果中所见。那么回到只存储指针呢? 正如我们上面提到的,没有 oo 的好处。
{
vector<Texture*> textures(100); // no constructors called
textures[1] = new Texture(640*480*32,"cube.jpg"); //no operators invoked on textures[1]
} // leaving scope but no destructors called on vector items ;(
最重要的是,当 vector 离开作用域时,不会自动调用析构函数。 可以手动完成,但这会重新引入错误的来源。
C++ 可以通过引入作用域版本的 new 来解决这个问题。 例如 new_local。 在这种情况下,编译器会在离开作用域时生成调用析构函数的代码,就像它现在对自动对象所做的那样。更新:我尝试在本文中实现它。
在当前 C++ 中,指向对象的指针的自动清理真的不可能吗?
现在考虑以下奇怪但完全有效的示例。请记住,堆栈是默认有限的(除非您在链接器设置中更改它)资源。 所以将这作为一个纯粹的学术示例,即自动在离开作用域时调用析构函数的指针数组是可能的。
struct A{
int no;
operator A* () { return this; } // just typecast
};
void array_of_objects_stored_by_pointers_with_autocleanup() {
A* a[10000]={0}; // an array of references which was not supposed to exist ?
a[7] = A(); a[7]->no=7; // no "new A()". Since "A()" is the same just not on heap but on stack
a[4] = A(); a[4]->no=4; // and auto cleanup of stack is build-in ;)
}
当通过指针存储的对象数组离开作用域时,它们会自动释放,而无需手动调用析构函数;
这是如何工作的?发生了什么?
= A() 在内部与 = new A(); 相同
调用的是相同的构造函数。唯一的区别是第一个使用堆栈作为分配器,第二个使用堆。
两者都返回指针。引用就是指针(只是满足某些条件才配得上“引用”这个标签),我们记得吗?
是的,对于堆指针(由 new 创建),有一个通过运算符重载(也称为智能指针)模拟指针的包装器的 hack,例如 std::shared_ptr 等。所以,如果你不介意将所有变量包装成功能隐藏、难以调试的宏/模板,那么这对你来说是一个很好的解决方案。
但我坚信简单的事情不应该被加密或隐藏,也不是每个变量都应该如此。
程序员应该尽可能了解发生了什么,就像在 C 语言中一样,而无需在脑海中构建模板和宏展开器。
如果你问 Linus 是否要混淆每个指向模板包装器的指针,他很可能会杀了你。
我记得 C 语言中对过度宏使用的强烈反对。 并且有合理的理由。
那个原因是“复杂性和隐藏代码逻辑是错误的根源”。
可能的最佳解决方案是修复 C++ 编译器 = 操作。
Skyscraper city[1000]; // instead of temp on any assign
city[1] = Skyscraper("Empire State Building"); // compiler should use &city[1] as this pointer
所以这个 在内部可以(通过显式的优化开关)优化成类似这样的东西
Skyscraper city[1000]; // we mark every element as in default constructor initialized state
try { // to enable this optimization default constructor can contain only memset calls to solve reinvocation problem
new (&city[1]) Skyscraper("Empire State Building");
} catch(...) { // in case of exception
new (&city[1]) Skyscraper() // we restore object to post default constructor state to keep standard behavior of C++ standard
throw; // and we rethrow any exception
}
这将修复动态和静态(对象内存)浪费 = 零分配/复制,因为元素只在已分配的内存中创建一次,就像出于性能原因它应该始终那样。
为什么静态(非动态)内存浪费同样重要,如果不是更重要?大多数对象是小的且独立的。 当你查看下面的基准测试时,存储包含静态声明数组的对象花费了 5662 毫秒,而存储包含动态数组的对象花费了 1372 毫秒。
此外,在对编译器进行此类更改后,所有使用大对象的旧代码在重新编译后都会以完全不同的速度运行。
因为我是一个好奇的人,我正在尝试在 Clang 这个出色的开源 C++ 编译器中实现和测试它,作为一个优化开关或 pragma。如果您想伸出援手,我将不胜感激。
http://clang-developers.42468.n3.nabble.com/Proposed-C-optimization-with-big-speed-gains-with-big-objects-tt4026886.html
但让我们专注于 C++ 的最新解决方案(不幸的是,仅适用于对象中的堆内存,并且需要大量代码更改)。
C++ 11 的新特性 移动语义
移动语义使您能够编写将动态分配的内存从一个对象传输到另一个对象的代码。移动语义之所以有效,是因为它能够将内存从临时对象(通过仅复制指针)传输到临时对象,而这些临时对象在程序中其他地方无法引用。不幸的是,大型静态声明(包含在对象内)的数组/结构/成员数据仍然必须无用地复制,因为如前所述,它们本身包含在即将被销毁的临时对象中。
为了实现移动语义,您通常需要为类提供一个移动构造函数,以及一个可选的移动赋值运算符=。源是(临时对象或不可更改的数据)的复制和赋值操作将自动利用移动语义。与默认的复制构造函数不同,编译器不提供默认的移动构造函数。
您还可以重载普通函数和运算符来利用移动语义。Visual C++ 2010 将移动语义引入了标准模板库 (STL)。例如,string 类实现了执行移动语义的操作。考虑以下连接多个字符串的示例。
string s = string("h") + "e" + "ll" + "o";
在 && 引用存在之前,每次调用 operator+ 都会分配并返回一个新的临时对象。operator+ 无法将一个字符串附加到另一个字符串,因为它不知道源字符串的内容是否可以被篡改(临时对象)或不能(变量)。如果源字符串都是变量,它们可能在程序中的其他地方被引用,因此不得修改。
但现在,由于 && 引用,我们知道传入的是一个临时对象(在程序中其他地方无法引用)。因此,operator+ 现在可以安全地将一个字符串附加到另一个字符串。这可以显著减少 string 类必须执行的动态内存分配次数。
当编译器无法使用返回值优化 (RVO) 或命名返回值优化 (NRVO) 时,移动语义也有帮助。在这些情况下,如果类型定义了移动构造函数,编译器会调用它。
再举一个例子,考虑将元素插入 vector 对象的示例。如果超出了 vector 对象的容量,vector 对象必须为它的元素重新分配内存,然后将每个元素复制到另一个内存位置以腾出空间给插入的元素。当插入操作复制元素时,它会创建一个新元素,调用复制构造函数将数据从前一个元素复制到新元素,然后销毁前一个元素。移动语义允许您直接移动对象,而无需执行昂贵的内存分配和复制操作。
所以,为了利用移动语义来实现对象高效插入 std::vector,您必须编写一个移动构造函数,以允许数据从一个对象移动到另一个对象。
所以,让我们看看在我们通常低效的城市复制运算符 = 示例中发生了什么,但实现了移动运算符 =嗯。除了您的复制运算符 = (&) ,您始终只是复制已赋值变量的数据。
现在您定义了一个额外的移动运算符 = (&&) ,当传入不可更改的数据(如分配给数组时创建的临时对象)时,将调用它。
Skyscraper city[1000];
city[1] = Skyscraper("Empire State Building");
- 您在城市外部分配大片空间 // 请注意 = Skyscrapper();
- 您在该空间中建造了建筑。
- 您只需将这座已建好的建筑标记为城市的一部分// 无需卡车(复制)
void operator = ( Skyscraper && in ) { // temp obj was passed in
mem = in.mem; // We copy just data pointer from temp (ie we move data)
size = in.size; // which is safe since temp obj can't change
in.mem = 0; // THIS IS KEY PART: we prevent deallocation when temp is destroyed
}
~Skyscraper() { if(mem) delete mem; } //BUT destructor must have delete as conditional
万岁,没有从临时对象分配/复制 = 终于没有浪费 CPU,也没有缓存被破坏
临时对象分配和初始化的内存不会被释放,因为它有了新的所有者。
对于完整的可编译示例,请将下面的基准测试代码复制到您选择的开发环境中。
不,对于那些认为上一个示例中一切都很清晰明了的人。
不要让眼睛欺骗你。
void operator = ( Skyscraper && in ) { // From now on in is of type Skyscraper & and not &&
next_proc(in); // Skyscraper & is passed in and is not movable anymore
next_proc((Skyscraper &&)in); // we need to explicitly retype it && temp to be movable again
}
Skyscraper && in 实际上不再是 && 类型。一旦进入函数,它就又变成了 &。所以
如果您想将 && 转发到另一个函数,您需要再次将其转换为 &&(在 STL 中通过 std::move)。为什么 C++ 决定在您不知情的情况下隐藏此功能?嗯,有人告诉我这是出于安全考虑。任何拥有名称的 && 都存在被代码中其他地方引用的风险,因此不认为它“可移动”状态是安全的。这似乎是我未完成的 C++ 业务,因为我无法想象在当前过程中引用局部变量。
还有一个鲜为人知的 ref-specifier 功能,您可以将运算符/方法限制为仅接受临时对象或仅接受变量。
struct string {
string& operator=(string const& other) & { /* ... */ }
};
现在,您再也不能说
string() = "hello";
不幸的是,我现在使用的 Visual Studio 2012 RC1 似乎还不支持这一点。
所以,总结一下。除非您使用包含大型静态声明的(对象内)结构/数组/成员,否则您的现有代码将获得显著的速度提升。尽管可以看到您的现有代码能提速多少(嗯……实际上您停止了它的减速),我创建了一个简单实用的 && 示例以及基准测试结果。但如果您不仅仅是在构造函数/析构函数中进行简单的 memset 操作,那么提速将显著更高。
基准测试结果:
- 存储包含数组的对象本身花费了 5662 毫秒 // 即使是 &&,这仍然是个问题
- 对包含数组的对象本身进行排序花费了 17660 毫秒// 这就是为什么你不应该存储对象
- 通过复制存储包含动态数组的对象花费了 1372 毫秒
- 通过移动(C++ 11)存储包含动态数组的对象花费了 500 毫秒
- 只存储对象指针花费了 484 毫秒
- 对对象指针进行排序花费了 0 毫秒
基准测试代码
为了了解通常的粗心赋值 = 有多糟糕。
我创建了一个示例,通过不同的方法将 500 个大型对象存储到数组中,并测量了所需时间。
Texture 代表我们在 C++ 中每天处理的标准大型对象 = 就是一堆数据,以及它的大小。
加上强制的 operator = 才能按值存储。现在它被简化到最少(没有链式常量等),只有简单的类型,这样你就可以只关注这两个运算符。 并且 sort 的 < 被反转以模拟最坏情况。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <algorithm>
using namespace std;
// current C++ Average Joe Object
struct Texture { int size; int color; void* mem;
Texture() : size(0), color(0), mem(0) {}
~Texture() {
if(mem) delete mem; mem = 0;
}
Texture ( int Size, int Color ) {
mem=new char[size=Size];
memset(mem,color=Color,size);
}
void operator = ( Texture & in ) { // variable passed in
color = in.color;
mem = new char[size=in.size]; // so we need copy
memcpy(mem,in.mem,in.size); // since variables change
}
};
// C++ 11 Enhanced version
struct Texture2 : virtual Texture {
void operator = ( Texture && in ) { // temp obj is passed in
color= in.color;
mem = in.mem; // We copy just data pointer from temp (ie we move data)
size = in.size; // which is safe since temp obj can't change
in.mem = 0; // THIS IS KEY PART: we prevent deallocation when temp is destroyed
}
};
// C++ 11 unfortunately cant solve the need to copy static(contained within) object data
// so no object can by stored in c++ via = efficiently without useless copy
// and even new operator && will not help since he solves only heap part of the problem.
// Point is. Don't use large statically declared arrays/structures/too many members
// or = with objects containing them if you care about speed.
struct Texture2StaticArray : Texture2 { // word static means statically declared
Texture2StaticArray() : Texture() {} // ie (contained within)
Texture2StaticArray( int Size, int Color ) {
memset(static_array,color=Color,sizeof(static_array));
}
char static_array[640*480*8];
};
#define TEXTURES 500
void store_objects_containing_static_array() {
Texture2StaticArray* texture =new Texture2StaticArray[TEXTURES];
DWORD start = GetTickCount();
for(int i=0;i<TEXTURES;i++) {
texture[i] = Texture2StaticArray(0,i);
}
printf("\nstore objects containing static array took %d ms", GetTickCount()-start );
start = GetTickCount();
sort(texture,texture+TEXTURES, [] (Texture2StaticArray& a, Texture2StaticArray& b) { return a.color > b.color; } );
printf("\nsort objects containing static array took %d ms", GetTickCount()-start );
delete [] texture;
}
void store_objects_containing_dynamic_array_current_standard() {
Texture texture [TEXTURES];
DWORD start = GetTickCount();
for(int i=0;i<TEXTURES;i++) {
texture[i] = Texture(640*480*8,i);
}
printf("\nstore objects containing dynamic array by copying took %d ms", GetTickCount()-start );
}
void store_objects_containing_dynamic_array_new_standard() {
Texture2 texture [TEXTURES];
DWORD start = GetTickCount();
for(int i=0;i<TEXTURES;i++) {
texture[i] = Texture(640*480*8,i);
}
printf("\nstore objects containing dynamic array by moving (c++ 11) took %d ms", GetTickCount()-start );
}
void store_pointers_to_any_object() {
Texture* texture [TEXTURES];
DWORD start = GetTickCount();
for(int i=0;i<TEXTURES;i++) {
texture[i] = new Texture(640*480*8,i);
}
printf("\nstore just pointers to objects took %d ms", GetTickCount()-start );
start = GetTickCount();
sort(texture,texture+TEXTURES, [] (Texture* a, Texture* b) { return a->color > b->color; });
printf("\nsort just pointers to objects took %d ms", GetTickCount()-start );
for(int i=0;i<TEXTURES;i++) { // We need to call destructors manually
delete texture[i];
}
}
void main() {
store_objects_containing_static_array();
store_objects_containing_dynamic_array_current_standard();
store_objects_containing_dynamic_array_new_standard();
store_pointers_to_any_object();
Sleep(-1);
}
现在,更善于观察的人可能会开始争论……
“这没什么新东西,我可以在当前标准的 C++ operator = & 中以同样的方式进行这种“移动”(或者只是在对象之间传递数据而不复制),那么我为什么还需要新的 && 运算符?
是的,你可以,也不可以。 如果你在 operator = & 中进行了像这样的移动,想象一下会发生什么。
... // This will not work as intended. Explanation bellow
void operator = ( const string & in ) {
mem = in.mem; // We move data by pointing to it
size = in.size;
in.mem = 0;
}
...
string a("white"),b("yellow");
string c=b; // c is "yellow" now
...
b="gotcha..." // but c is now "gotcha..." too -> should not happen !!!
如果我们移动了 =,只复制标准 operator = & 中的数据指针。
那么每当 b 改变时,c 也会改变;
这不是预期的。
- 所以我们实际上想在从可能改变的数据(变量等)赋值时进行复制。
- 我们只是复制我们确定不会改变的数据的指针。
不幸的是,直到 C++ 11,& 无法区分传递的数据是否可以改变。
因此,出于 c=b 示例中所解释的原因,移动在当前 C++ 标准中是不可能的。
反过来,新的 && 可以区分传递的数据是不可变的,因此是安全的。
只需指向其数据即可跳过复制。
所以,总结一下。
在新的 C++ 11 标准中,您现在应该维护两套运算符和构造函数。
- operator = 和构造函数采用 & ,您从可能更改的数据(变量等)复制。
- operator = 和构造函数采用 && ,您只需指向不会更改的数据,通过跳过复制来节省内存和 CPU(临时对象等)
不幸的是,这意味着您现在必须为几乎所有声明为通用复制/粘贴代码的运算符实现两套,但仍然只修复了性能问题的堆方面。
所以重新考虑使用 = 操作对象。
至少直到编译器编写者通过对可移动对象上的 = 进行内部 placement new 来修复堆和静态内存浪费。
不幸的是,这意味着您现在必须为几乎所有声明为通用复制/粘贴代码的运算符实现两套,但仍然只修复了性能问题的堆方面。
所以重新考虑使用 = 操作对象。
至少直到编译器编写者通过对可移动对象上的 = 进行内部 placement new 来修复堆和静态内存浪费。
string b(a); //compiler invokes constructor (&) on b and we make copy since a can change
string c=b; //compiler invokes operator = & on c and we make copy since b can change
string texts[10];
texts[1]= string("white fox"); //compiler invokes =&& on texts[1] since temp obj was passed in
为什么它被称为右值引用 &&
现在,整个文章我特意没有使用 rvalue(不能更改)和 lvalue(可以更改)这样的术语,因为它们并不像它们的名称所暗示的那样。
lvalue 应该被命名为“变量”之类的名称。
rvalue 应该被命名为“临时”之类的名称。

它们只是技术语法遗迹,会使人感到困惑。
它们源自 C 语法如何在 lex 和 yacc 中描述,例如在
“那个特定的语法规则,它们位于 = 左边或右边” BUT 那个特定的规则可以是一个更大表达式的一部分,而 lvalue 突然变成了 rvalue。
或者让我这样解释。
任何没有名字的都是右值,否则就是左值。
小心