通过”移动来改善smart_ptr行为”
template <typename T>
class smart_ptr {
…
smart_ptr(smart_ptr&& other)noexcept//拷贝构造参数改为右值引用 变为移动构造函数
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs)//不传入引用而传入对象
{//传入对象构造形参rhs时
rhs.swap(*this);
return *this;
}
//原
//smart_ptr& operator=(smart_ptr& rhs)
//{
//smart_ptr(rhs).swap(*this);
//return *this;
//}
…
};
1 把拷贝构造函数中的参数类型 smart_ptr& 改成了 smart_ptr&&;现在它成了移动构造函数。
2 把赋值函数中的参数类型 smart_ptr& 改成了 smart_ptr,在构造参数时直接调用拷贝构造构造形参(原参数是smart_ptr&对象引用 引用形参是不需要调用构造函数去构造的—因为引用的本质是指针,改为了smart_ptr对象类型 是需要构造形参的—如果传入实参的是左值则调用拷贝构造,如果传入的实参是右值则调用移动构造 去构造形参),从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造形参时传入的实参是左值还是右值来决定走的是移动构造还是拷贝构造。
根据 C++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者(拷贝构造 包括系统默认的那个拷贝构造)自动被禁用(记住,C++ 里那些复杂的规则也是为方便编程而设立的)。
侯捷老师右值引用相关内容
右值引用是为了解决不必要的拷贝(用一个临时对象构造一个对象),特别是vector在grow up时发生的搬移动作,在移动构造出现之前 搬移动作调用的都是拷贝构造这将耗费大量时间(拷贝原来的堆上内存数据到另一块堆上内存,并将原来的堆上内存释放 —- 这一系列操作其实都是没必要的操作)
左值 可以出现在op=左边(也可以出现在右边) 右值 只能出现在op=的右边 (右值的特点是 不具名—匿名—临时 临时对象一定是右值 函数返回一个对象 注意不是对象引用而是对象 这个返回对象值也是匿名的)
当赋值=的右边是个右值(字面值 临时对象 临时对象的引用)时通过移动,=左边的对象”偷”到等号右边对象的资源,而不是去执行赋值拷贝。
1
//字符串是字面值 但是它是左值
string 内含堆上内存指针
string s1(“hello”);string s2(“world”);
s1+s2 = s2;//正确 在这句话后cout s1,s2内容 s1,s2的内容依旧没变
string() = “world”;//正确 竟然可以对临时对象赋值!
//标准库的complex
//complex 内不含堆上内存指针
complex<int> c1(3,8),c2(1,0);
c1+c2 = complex<int> (4,9);//正确 在这句话后cout c1,c2内容 c1,c2的内容依旧没变
complex<int>() = complex<int> (4,9);//正确 complex竟然也可以对临时对象赋值!
这两个类这样写能通过编译 其实是标准库作者的失职 不应该能通过
2
int foo(){return 5;}
..
int x = foo();//ok
int* p = &foo();//编译错误 取函数返回值的地址
foo() = 7;//编译错误
在c++11前无法取右值的地址,函数的返回值就是右值
右值天经地义要出现在op=的右边,c++11认为对其进行资源的转移 转移到op=左边,比传统的拷贝构造更合理。
1 告诉编译器这是个右值 —- 临时对象一定是右值 , 或者将左值通过move变为右值
2 写一个专门处理 右值移动的 op=重载
class MyString{
private:
char* data_;
public:
//拷贝构造
MyString(const MyString& str):初始化列表
{
创建一块新的堆上内存给this->data_,将str的data_内容拷贝到this的data_
内
}
//移动构造 资源指针的浅拷贝
MyString(MyString&& str) noexcept
:初始化列表
{
将this的data指针指向str的data指针所指向的那块内存,并将str的data指向
null与那块内存断开连接。相当于this“偷了”str的data堆上内存
}
};
//测试移动
template<typename M>
void test_moveable(M c,long& value)//调用 test_moveable(vector<MyString>(),3000000L);
{
//向vector<MyString>()容器中 插入3000000个新元素
char buf[10];
typedef typename iterator_traits<typename M::iaterator>::value_type Vtype;
//计时
clock_t timeStart = clock();
for(long i = 0;i<value;++i)
{
snprintf(buf,10,”%d”,rand());//将得到的随机数 转为字符串 并存到buf中
auto ite = c.end();//容器尾端
c.insert(ite,Vtype(buf));//在容器尾端插入 新的这个字符数据MyString临时对象
// Vtype(buf) 构造出来的是个临时对象 是匿名的 是个右值
// Vtype(buf)为匿名对象 生命周期只在这一行 这行结束后自动调用析构函数
//vector的insert有两个版本 insert(...,&x)拷贝版 insert(...,&&x)移动版(c++11后)
//传入的是个临时对象 右值 会调用insert(...,&&x)移动版
//在构造右值引用形参&&x时 会调用MyString的移动构造去构造形参x
//如果Vtype有写移动构造 则调用移动构造 否则调用拷贝构造
}
cout<<”ms:”<<clock()-timeStart<<endl;
cout<<”size:”<<c.size<<endl;
}
M c1(c);//拷贝构造
//当我们清楚 接下来不会再用某个左值,但这个左值有堆上资源,则我们可以通过move将左值强制变为右值,去调用移动构造 用新的变量去偷这个不再用到的左值的资源
M c2(std::move(c1));//移动构造 后续因为c1的堆上指针断开了,不能再使用c1
c1.swap(c2);//调用标准库容器的swap直接交换容器
//拷贝非常耗时s级,移动和swap us级
template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept
{return static_cast<typename std::remove_reference<_Tp>::typr&&>(__t);}
vector insert行为
G2.9
iterator insert(iterator position,const T& x){
size_type n = position - begin();
if(finish !=end_of_storage&&position == end())
{
construct(finish, x);
++finish;
}
else
insert_aux(position,x);
return begin()+n;
}
G4.9
iterator insert(const_iterator __position,const value_type& __x);//构造x调用的是拷贝构造
iterator insert(const_iterator __position,value_type&& __x)//构造x调用移动构造 move版
{ return emplace(__position,std::move(__x));}
这里要强调一下c.insert(ite,Vtype(buf));
是先调用insert(…,&&x) ,给匿名对象 Vtype(buf)赋名x,接下来因为x有名了 所以insert内以x做形参的其他函数都不会调用&&x右值引用版本。类似于转交的动作,转交时会发生重要信息的遗漏—-转交传递动作是不完美的 not perferct forward