移动构造函数
为啥要有移动构造函数, 个人浅见,为避免调用成员的复制构造函数造成额外的开销,毕竟复制指针要来的更方便快捷,因而产生的。比如一个string对象,普通的拷贝赋值或者拷贝构造函数的方式中,我们是在内存空间里创建一个新的string对象,并将原string的char数组一个一个复制到新的string对象。移动构造或者移动赋值的时候,我们先假定原来的string,我们已经不需要用了。但是原来string对象里面的指向char的指针我们需要继续保留,不能让编译器在析构的时候,将这一指针指向的内存释放掉。那这个时候为了”废物利用”, 我们就直接将原来string的char指针给复制过来,同时让原来string的char指针指向nullptr。这样原来的string有价值的部分已经被瓜分干净了,可以被编译器”垃圾回收”了
//代码中以自定义结构体Str为基本元素,并对其实现移动构造和移动赋值操作,可以完美展现std::move和 普通赋值的差异
class Str {
public:
Str(int t = 0) :i(new int(t))
{
cout << "Here Str construct" << endl;
}
Str(const Str & t)
{
cout << "Here Str copy construct" << endl;
i = new int(*t.i);
}
Str(Str && t)noexcept:i(t.i)//noexcept 通知标准库该函数不会抛出异常,否则标准库会认为移动时可能有异常,并且为了处理这种可能而做一些额外的操作
{
cout << "Here Str move construct" << endl;
t.i = nullptr;
}
Str& operator=(Str&& p) noexcept
{
if (this != &p)
{
delete i;
i = p.i;
p.i = nullptr;
}
cout << "Here Str move assign" << endl;
return *this;
}
Str& operator=(Str& p)
{
auto tt = new int(*p.i);
delete i;
i = tt;
cout << "Here Str assign" << endl;
return *this;
}
~Str()
{
if (i)//****当reallocate函数里以移动的方式进行构造时,旧的Str会在移动拷贝时i被设置为nullptr,因而不会进这个条件语句。而拷贝赋值的时候,旧的Str的i还存在,所以会进入条件语句,进行内存的释放
{
delete i;
i = nullptr;
cout << "Here Str actual delete" << endl;
}
cout << "Here Str delete" << endl;
}
private:
int *i;
};
class StrVec
{
public:
StrVec():elements(nullptr),first(nullptr),cap(nullptr)
{
}
StrVec(const StrVec & p)
{
auto t = alloc_n_copy(p.begin(), p.end());
elements = t.first;
first = cap = t.second;
}
void push_back(const Str &str)
{
chk_n_size();
alloc.construct(first++, str);
}
StrVec &operator=(const StrVec &p)
{
auto t = alloc_n_copy(p.begin(), p.end());//防止自拷贝
free();
elements = t.first;
first = cap = t.second;
return *this;
}
Str * begin()const
{
return elements;
}
Str * end()const
{
return first;
}
~StrVec()
{
free();
}
std::size_t size()
{
return std::distance(elements, first);
}
std::size_t capacity()
{
return std::distance(elements, cap);
}
private:
std::pair<Str *, Str *> alloc_n_copy(const Str *b, const Str *e)
{
auto data = alloc.allocate(e - b);
return{ data, std::uninitialized_copy(b, e, data) };
}
void free()
{
if (elements) //确保 elements不为空
{
for (auto p = first; p != elements;)
{
alloc.destroy(--p);
}
alloc.deallocate(elements,cap-elements);
}
}
void reallocate() //移动构造函数
{
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for (std::size_t i = 0; i != size(); i++)
{
alloc.construct(dest++, std::move(*elem++)); //********这种方式是以移动构造的方式创建对象
// alloc.construct(dest++, *elem++);//*******这种是直接拷贝赋值的方式创建对象
}
free();
elements = newdata;
first = dest;
cap = elements + newcapacity;
}
void chk_n_size()
{
if (size() == capacity())
{
std::cout << "Here call reallocate" << endl;
reallocate();
std::cout << "Here reallocate finished" << endl;
}
}
static std::allocator<Str> alloc;
Str *elements;
Str *first;
Str *cap;
};
std::allocator<Str> StrVec::alloc;
int main()
{
Str data1{
1};
Str data2{
2};
StrVec s1;
std::cout << "============" << std::endl;
s1.push_back(data1);
std::cout << "============" << std::endl;
s1.push_back(data2);
std::cout << "============" << std::endl;
}
可以看出移动构造函数由于保留了原来Str的int指针成员,并将原Str的int*成员设置为nullptr,所以析构的时候,没有进入if的条件语句内部
只有当类没有定义任何自己版本的拷贝控制成员,并且它的所有数据成员都能移动构造或者移动赋值时,编译器才会为它合成移动构造函数或者移动赋值运算符。
类成员的移动构造函数是删除的或者(没有自己的移动构造函数且编译器不能为其合成时),则该类的移动构造函数是删除的。
没有析构函数,也就没有移动构造函数
如果有成员是引用或者const成员,则移动赋值运算符也是删除的。
如果类定义了移动构造函数或者移动赋值运算符, 则编译器不会再为该类合成拷贝构造函数或拷贝赋值运算符。换句话说,定义了移动构造函数,必须定义自己的拷贝构造函数或者拷贝赋值运算符。
如果一个类定义了拷贝构造函数,但没有定义移动构造函数,则其对象是通过拷贝构造函数来完成移动操作的
{
public:
A(){}
A(A&)
{
cout << "111" << endl;
}
};
int main()
{
A s;
A s2 = std::move(s);
}
区分移动和拷贝的重载函数通常是一个版本接受一个const T&, 而另一个版本接受T&&.
参数列表后面添加引用限定符。一个函数可以同时用const 和& 限定,在此情况下,引用必须在const 之后。如果有多个同名函数,只要有一个有引用限定符,则其他函数必须也需要引用限定符。左值是const &, 右值是&&。
class A
{
public:
A &operator=(const A &)&;//只能向可修改的左值赋值
A sorted()const &;
A sorted()&&;//有引用限定符,因此上面的左值sort也需要有引用
private:
std::vector<int> data;
};
A A::sorted()const &
{
A ret(*this);
sort(ret.data.begin(),ret.data.end());
return ret;
}
A A::sorted()&&
{
std::sort(data.begin(), data.end());
return *this;
}
移动迭代器
移动迭代器生成右值引用。可以通过标准库std::make_move_iterator()来实现
右值引用
左值右值:左值具有持久的状态,而右值要么是字面值,要么是表达式求值过程中创建的临时对象。
std::move(rr1)//调用move函数意味着除了对rr1 销毁或者重新赋值,将不再使用它