c++11特性 对象移动
目录
1.右值引用
2.移动构造函数和移动赋值运算符
3右值引用和成员函数
介绍
先说下左值和右值:左值表达式表示的是对象,右值表达式表示的是对象的值。
新标准中的包含了一个重大特性,在以前我们编写的程序,其中会产生大量不必要的拷贝,可能会极大的影响程序的效率。
所以c++11引入了“对象移动”这一特性。
简单理解就是移交一些临时变量的所有权,在程序中一些变量的生存期非常短,比如 部分表达式产生的值,后增运算符i++,函数返回值等等,而我们可以在他们即将销毁之前
把他们的所有权“移动”(转移)给其他变量,它们在内存中的位置是不变的,也就是没有创建新的对对象,只不过归属于其他变量了,生存期也就随着所属的变量而变化了。
为了支持移动操作,从而引入了右值引用。
#include <iostream>
#include <string>
std::string foo()
{
return std::string("abc");
}
int main()
{
std::string s3 = foo();
const std::string &s = foo();
std::string &&s2 = foo();
}
这段代码中,foo( )返回了一个临时变量,如果我们想使用这个临时变量的话,一般创建一个新变量来保存,也就是s3,调用了拷贝构造函数,效率低
当然我们可以仅仅使用常量引用像s那样,但是我们只能使用这个值而不能修改它
最后一个s2使用右值引用,它把foo( )返回临时变量的所有权(移动)交给了s2,没有调用拷贝构造函数。效率较高,而且不像引用不能修改。
这只是一个简单的例子,当我们多出的拷贝操作的对象非常大的情况下,效率就会差很多了。
1.右值引用
右值引用&&,只能绑定到将要销毁的对象,且对象没有其他用户。所以我们可以将即将销毁的对象通过右值引用“移动”到新的变量中,帮它找到个“归属”。右值引用也仅仅是个变量!
注意:变量表达式都是左值,so不能将右值引用绑定到右值引用(已经是变量了)上。写代码时分清左值右值即可。
int &&r1 = 42;
int &&r2 = r1; error
结论:右值引用可以自由的接管所引用对象的资源
<1.标准库move函数
上面说了不能将右值引用绑定到右值上,因为右值引用也是一个变量,是一个左值
那么解决办法就是标准库引入的move函数,它可以把左值转化为一个对应的右值引用类型。
int &&r1 = 42;
int &&r2 = std::move(r1);
使用move的前提是我们不再关心移后源对象,也就是r1.
注意:右值引用发生后就变成左值了。
具体使用
2.移动构造函数和移动赋值运算符
移动构造函数:
如果我们的类同时支持移动和拷贝,那么我们可以移动资源而不是拷贝资源
#include <iostream>
#include <string>
#include <utility>
class foo
{
public:
foo():p(nullptr) { }
foo(const std::string &s):
p(new std::string(s)) { std::cout << "构造函数" << std::endl;}
foo(const foo&f):
p(new std::string(*f.p)) { std::cout << "lvalue" << std::endl;}
foo& operator=(const foo&f)
{
std::cout << "=" << std::endl;
if(!p)
{
p = new std::string(*f.p);
}
return *this;
}
foo(foo &&f)noexcept //noexcept 表示不抛出任何异常
:p(f.p)
{
std::cout << "rvalue" << std::endl;
f.p = nullptr;
}
private:
std::string *p;
};
foo f()
{
return foo("asd");
}
int main()
{
foo f1("asd");
foo f2(f());
}
上面的运行结果是编译器经过优化的,下面的结果是不优化,加参数-fno-elide-constructors的
例子比较简单,复杂情况下编译器就不会这样优化了。
补充:当时加入了调试信息first,second等等是想看哪个先执行,可以忽略,结果粘贴代码就把调试删除了,截图忘了
- -。
f( )函数return foo("asd");
正常来说会先创建一个foo对象,然后返回一个右值。
但是编译器会默认有返回优化(NRVO(命名返回值优化), RVO)。
图片第一个编译器优化就是调用两个普通的构造函数。
下面第二个就是加上-fno-elide-constructor编译参数忽略(NRVO,RVO),那个就会显示的调用移动构造函数。
f( )一次,foo f2(f( ))一次
注意:
移动构造函数不分配任何内存,它只是转移了,移后源对象要保证销毁它是无害的。
noexcept 是新标准引入的,不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
为什么要添加noexcept,在一般的拷贝构造函数中,实际上是新创建了一个对象,将源资源拷贝新创建的对象中,如果抛出异常,也可以保证源资源不变化,
但是移动构造函数就不一样了,使用的是一份资源,如果中间抛出异常,不能保证源资源的不变,所以我们显示指出不能抛出异常。
移动赋值运算符:
foo& operator=(foo &&f)noexcept
{
std::cout << "r=" << std::endl;
if(!p) //处理通过move的自赋值情况。
{
p = f.p;
f.p = nullptr; //设置处于可销毁状态
}
return *this;
}
注意:
有时我们会销毁移动源对象,所以确保移动后的移动源对象处于可销毁状态。
合成的移动操作:
只有当类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数和移动赋值运算符。
移动操作永远不会隐式定义为删除的函数
将合成移动操作定义为删除函数的条件:
1.有类成员定义了自己的拷贝构造函数且为定义移动构造函数,或类未定义自己的拷贝构造函数编译器不合成移动构造函数,移动操作符类似。
2.如果有类的成员的移动操作定义为删除的,那么此类的移动操作也定义为删除的。
3.类的析构函数被定义为删除的
4.如果有类的成员是const或是引用
定义了一个移动构造函数或移动操作运算符的类必须定义自己的拷贝操作,否则,这些成员默认被定义为删除的。
如果一个类没有移动操作,即使我们通过move它也只会使用拷贝操作。
巧妙的实现赋值操作符
通过swap和参数的改变巧妙的用一个赋值运算符实现了拷贝赋值操作和移动赋值操作
#include <iostream>
#include <string>
class foo
{
friend void swap(foo &f1, foo &f2); //我们自己定义的swap操作
public:
foo():p(nullptr) { }
foo(const std::string &s):
p(new std::string(s)) { }
foo(const foo& f):
p(new std::string(*f.p)) { }
foo(foo &&f):
p(f.p) { f.p = nullptr; }
foo& operator=(foo f) //同时实现了拷贝赋值操作符和移动赋值操作符
{
std::cout << "=" << std::endl;
swap(*this, f);
return *this;
}
private:
std::string *p;
};
void swap(foo &f1, foo &f2)
{
using std::swap; //避免使用了自己的swap而造成无限递归
swap(f1.p, f2.p); //系统的swap
}
int main()
{
foo f1;
foo f2;
foo f3("asd");
f1 = f3;
f2 = std::move(f3);
}
新的三五法则:
所有的五个拷贝控制成员应该看成一个整体,如果类定义了任何一个拷贝操作,它就应该定义所有的五个操作。
移动迭代器
新标准库定义了一种移动迭代器适配器,一个迭代器的解引用运算符返回一个指向元素的左值,与其他迭代器不同,移动迭代器解引用运算符生成一个右值引用。
我们通过调用make_move_iterator函数将一个普通迭代器转换为一个移动迭代器,此函数接受一个普通迭代器参数,返回一个移动迭代器。
uninitialized_copy(make_move_iterator(begin()),
make_move_iterator(end()),
first);
我们只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。注意:
在移动构造函数和移动赋值运算符这些类实现代码之外的地方,只有当你确信需要进行移动操作且移动操作是安全的,才可以使用std::move。
!拷贝并交换运算符不如拷贝构造函数匹配率高。
右值引用和成员函数
除了构造函数和赋值运算符外,如果一个成员函数同时提供拷贝和移动版本,也能从中受益。
void fun(const X&a);
void fun(X &&a); //右值传参
右值和左值引用成员函数
在旧标准中,我们不能阻止右值像右值赋值这种形式,新标准仍然允许向右值赋值,但是我们可能希望在自己的类中阻止这种用法,强制左侧对象是一个左值。
和定义常量成员函数后面添加const相同,我们可以在后面添加引用限定符。
引用限定符号可以是& 或 &&,分别指出可以指向一个左值或一个右值。
类似const成员函数,引用限定符只能用于(非static)成员函数,且必须同时出现在函数声明和定义中。
对于&限定的函数,我们只能将它用于左值,对于&&限定的函数,我们只能将它用于右值。
如果此函数还有const限定,&或&&必须跟在const后面。
class foo
{
foo& operator=(const foo)&;
...
foo fun1();
foo& fun2();
};
foo i;
fun1() = i; //error只能赋值给左值
fun2() = i;//yes
当我们定义const成员函数时,可以定义两个版本,唯一的差别是一个版本有const限定而另一个没有,引用限定函数则不一样,如果我们定义两个或两个以上
具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加。也就是如果一个成员函数有引用限定符,那么具有相同参数
列表的所有版本都必须有限定符。
举个书上的例子方便理解:
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <string>
class Foo
{
friend std::istream& operator>>(std::istream&is, Foo &f);
friend std::ostream& operator<<(std::ostream&os, Foo &f);
public:
Foo sorted() &&; //右值
Foo sorted() const&; //左值
private:
std::vector<std::string> data;
};
std::ostream& operator<<(std::ostream&os, Foo &f)
{
for(const std::string &s : f.data)
{
os << s << " ";
}
return os;
}
std::istream& operator>>(std::istream&is, Foo &f)
{
std::string s;
while(is >> s && s != "q")
{
f.data.push_back(s);
}
if(is.fail())//若流处于fail状态,返回true
f = Foo();
return is;
}
Foo Foo::sorted()&&
{
std::cout << "&&" << std::endl;
std::sort(data.begin(), data.end());
return *this;
}
//三个左值版本的sorted
//Foo Foo::sorted()const& //段错误,递归栈溢出
//{
// std::cout << "&" << std::endl;
// Foo ret(*this);
// return ret.sorted(); //ret是左值,不断的调用自己
//}
Foo Foo::sorted()const& //正确
{
std::cout << "&" << std::endl;
return Foo(*this).sorted(); //强制类型转换后产生右值,调用右值版本
}
//Foo Foo::sorted()const& //正确
//{
// std::cout << "&" << std::endl;
// Foo ret(*this);
// std::sort(ret.data.begin(), ret.data.end());
// return ret;
//}
int main()
{
Foo f1;
std::cin >> f1;
std::cout << f1 << std::endl;
f1.sorted();
//std::move(f1).sorted(); //调用右值版本
}
可见第二个左值版本中调用了右值版本
完