我刚听完Scott Meyers关于C++0x的软件工程电台播客访谈。大多数新特性对我来说都是有意义的,我现在对C++0x感到兴奋,除了一个。我还是不懂移动语义。。。到底是什么
我发现用示例代码最容易理解移动语义。让我们从一个非常简单的字符串类开始,该类只保存指向堆分配内存块的指针:
#包括<;cstring>;
#包括<;算法>;
类字符串
{
字符*数据;
公众:
字符串(常量字符*p)
{
尺寸=标准::斯特伦(p)+1;
数据=新字符[大小];
标准::memcpy(数据、p、大小);
}
因为我们选择自己管理内存,所以我们需要遵循三条规则。我将推迟编写赋值运算符,现在只实现析构函数和复制构造函数:
~string()
{
删除[]数据;
}
字符串(常量字符串和该字符串)
{
size\u t size=std::strlen(即.data)+1;
数据=新字符[大小];
std::memcpy(数据,即数据,大小);
}
复制构造函数定义复制字符串对象的含义。参数const string&;绑定到字符串类型的所有表达式,允许您在以下示例中进行复制:
字符串a(x);//第1行
字符串b(x+y);//第2行
字符串c(一些函数返回字符串());//第3行
现在是对移动语义的关键洞察。请注意,只有在我们复制x的第一行中,这种深度复制才是真正必要的,因为我们可能想稍后检查x,如果x发生了某种变化,我们会非常惊讶。你注意到我刚才说了三次x(如果包括这句话,则为四次)并且每次都表示完全相同的对象?我们称x等表达式为“左值”
第2行和第3行中的参数不是左值,而是右值,因为底层字符串对象没有名称,因此客户端无法在稍后的时间点再次检查它们。
右值表示在下一个分号处被销毁的临时对象(更精确地说:在词汇上包含右值的完整表达式的末尾)。这一点很重要,因为在初始化b和c期间,我们可以对源字符串执行我们想要的任何操作,客户端无法分辨出不同之处
C++0x引入了一种称为“右值引用”的新机制,
允许我们通过函数重载检测右值参数。我们所要做的就是编写一个带有右值引用参数的构造函数。在该构造函数中,我们可以对源代码执行所需的任何操作,只要我们将其保持在有效状态:
string(string&;that)//string&;是对字符串的右值引用
{
数据=那是数据;
即.data=nullptr;
}
我们在这里做了什么?我们没有深入复制堆数据,而是复制了指针,然后将原始指针设置为null(以防止源对象的析构函数中的“delete[]”释放我们的“刚刚窃取的数据”)。实际上,我们是“窃取”的最初属于源字符串的数据。同样,关键的洞察是,在任何情况下,客户端都无法检测到源已被修改。由于我们在这里没有真正进行复制,因此我们将此构造函数称为“移动构造函数”。其工作是将资源从一个对象移动到另一个对象,而不是复制它们
祝贺你现在明白了移动语义学的基本原理,让我们继续执行赋值操作符。如果你不熟悉拷贝和交换习语,学它,然后回来,因为它是一个很棒的与异常安全相关的C++习语。
字符串和运算符=(字符串)
{
std::swap(数据,即.data);
归还*这个;
}
};
哈,就是这样?“右值参考在哪里?”你可能会问。“我们这里不需要它!”这是我的回答:)
请注意,我们通过值传递参数that,因此that必须像任何其他字符串对象一样进行初始化。那将如何初始化?在旧的C++98时代,答案应该是“由复制构造函数”。在C++0x中,编译器根据赋值运算符的参数是左值还是右值,在复制构造函数和移动构造函数之间进行选择
因此,如果您说a=b,那么复制构造函数将初始化该(因为表达式b是左值),赋值运算符将内容与新创建的深度副本交换。这就是复制和交换习惯用法的定义——制作一个副本,用副本交换内容,然后通过离开范围来摆脱副本。这里没什么新鲜事
但是如果你说a=x+y,那么移动构造函数将初始化(因为表达式x+y是一个右值),因此不涉及深度复制,只涉及有效移动。
表示仍然是独立于参数的对象,但其构造非常简单,
因为堆数据不需要复制,所以只需移动即可。没有必要复制它,因为x+y是一个右值,同样,可以从右值表示的字符串对象中移动
总之,复制构造函数生成深度复制,因为源必须保持不变。
另一方面,move构造函数可以只复制指针,然后将源中的指针设置为null。可以用这种方式“取消”源对象,因为客户端无法再次检查该对象
我希望这个例子能把要点讲清楚。还有很多关于右值引用和移动语义的内容,为了保持简单,我特意省略了这些内容。如果你想知道更多详情,请参阅我的补充答覆