这一块比较难 初步做一个笔记 希望将来能有更深的理解
考虑如下代码:
std::string func(std::string str){
return str;
}
int main(){
std::string str = func("sadjals");
system("pause");
return 0;
}
上面这段代码会发生很多次string对象的拷贝. 首先字符串常量转换成函数里的临时变量需要一次, 返回值赋给对象str时, 如果编译器不做返回值优化(Return value optimization)的话, 则还需要创造一个temp临时变量接受函数返回值, 再将temp赋给str. 对于函数参数的拷贝, 我们可以用引用或者指针. 而对于std::string str = func("sadjals");
, func("sadjals")
是后文再也用不到的变量, 那我们能不能希望不要通过中间的媒介, 而是直接将func的返回值移动到被赋值的对象? 这就是移动语义的由来吧.
在说明移动语义之前, 应该有必要说明右值引用.
右值引用就是一种特殊的引用, 它和左值引用可以说井水不犯河水. 例如, 以下代码非法:
int i = 42;
int && r = i; // Error!! 右值引用不能指向左值
当然, 常量的左值引用也可以指向右值, 例如:
const int & l = 42; // 合法
考虑开头的那个例子. 在赋值的时候, 如果我们重载一个=运算符, 让我们实现之前说的"移动"思想, 而不是逐元素拷贝, 那么一个直接的想法是我们对当前对象, 接管右值的所有权, 然后把右值废掉, 相当于把值偷了过来, 这样就避免了拷贝带来的复杂度.
class Student{
private:
std::string name;
int* scores;
int length;
public:
Student(const std::string& name_, int* scores_, int length_) : name(name_), length(length_) {
this->scores = new int[length_];
for (int i = 0; i < length_; ++i){
this->scores[i] = scores[i];
}
}
Student(const Student& stu) : name(stu.name), length(stu.length) {
std::cout << "Copy constructor of " << this->name << " called\n";
this->scores = new int[length];
for (int i = 0; i < length; ++i){
this->scores[i] = scores[i];
}
}
Student(Student&& rStu): name(rStu.name), length(rStu.length){
std::cout << "Move constructor of " << this->name << " called\n";
this->scores = rStu.scores; // 窃取
// 销毁
rStu.scores = nullptr;
rStu.name = "";
rStu.length = 0;
}
Student& operator+(const Student& stu2){
for (int i = 0; i < this->length; ++i){
this->scores[i] += stu2.scores[i];
}
return *this;
}
Student& operator=(Student&& stu2){
if (this == &stu2) return *this;
// 执行与移动构造相似的流程
std::cout << "overload operator = " << this->name << " called\n";
this->scores = stu2.scores; // 窃取
// 销毁
stu2.scores = nullptr;
stu2.name = "";
stu2.length = 0;
return *this;
}
virtual ~Student() { std::cout << "Deconstructor of " << this->name << " called\n"; delete[] this->scores; }
};
void func(){
int arr0[] = {100, 20, 59, 59, 59};
Student s0 ("Sunxiaochuan", arr0, sizeof(arr0) / sizeof(int));
Student s1 ("Dasima", arr0, sizeof(arr0) / sizeof(int));
Student s2 = static_cast<Student&&>(s0 + s1);
// Student s2 = s0 + s1;
}
在上述代码的移动构造函数和=重载函数中, 首先将右值的指针所指向的内存接管过来, 然后将右值的指针释放, 相当于窃取, 为了代码的鲁棒性, 右值对象的其余值应该都设为0.
注意:
Student s2 = static_cast
之所以用强制类型转换, 是因为如果不加的话编译器有返回值优化, 会调用拷贝构造函数. 为了方便说明, 故将其强制转换为右值. 实际上, 这一句的作用与后文提到的(s0 + s1); std::move()
相同
调用func()函数, 输出如下:
Move constructor of Sunxiaochuan called
Deconstructor of Sunxiaochuan called
Deconstructor of Dasima called
Deconstructor of called
解释:
Student s2;
s2 = static_cast<Student&&>(s0 + s1);
观察上面的student类, 里面包含了string类作为对象. 在student类的移动构造函数中, 按照上面的写法:
Student(Student&& rStu): name(rStu.name), length(rStu.length){}
则在string的类还是会调用拷贝构造, 这时比较低效的. 因此, 我们可以利用std::move()
强制转换:
Student(Student&& rStu): name(std::move(rStu.name)), length(rStu.length){
std::cout << "Move constructor of " << this->name << " called\n";
this->scores = rStu.scores; // 窃取
// 销毁
rStu.scores = nullptr;
rStu.name = "";
rStu.length = 0;
}
点进move的源码:
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
我们发现, 正如前文所说, 其就相当于static_cast强制转换为右值.
python中有一个map函数, 可以将一个函数作用于任意参数, 例如:
a = map(int, [2.3, 1.5, 0.6])
输入:
list(a)
输出:
[2, 1, 0]
那我们在C++中能实现类似的功能吗?
要实现这样的功能, 就一定要利用模板. 那如果采用这样的方式:
template<typename T>
void function(T t) {
func(t);
}
其中func是另一个函数. 这样看似可以, 但是有一个问题, 那就是不论调用function时传入的t是左值还是右值, 到了函数里都成为了左值, 会进行额外的拷贝操作.
为什么要保持左值右值的一致性? 因为左值或右值的传递, 直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。
能够保持左值右值一致性, 就叫做完美转发.
如果我们想保持左值右值不变, 就要利用到万能引用.
万能引用很简单, 只需要两个&&号声明即可, 修改为:
template<typename T>
void function(T&& t) {
func(t);
}
这时, 编译器会自动推断t为左值还是右值, 推断的原则叫做引用折叠规则:
但凡有左值引用参与的, 就推断成左值引用. 也即只有当t是右值引用时, 才推断为右值引用.
即:
t不是引用时, 推断为右值引用
t为左值引用时, 推断为左值引用
t为右值引用时, 推断为右值引用
但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢(真正的"完美"转发?)?
只需使用std::forward即可. 其作用是将原本的引用属性保持不变.
因此, 我们可以写出如下程序:
template <typename Func, typename... Args>
auto myMap(Func&& f, Args&& ... args){ // 万能引用
/*
Func: 函数指针等可调用的对象
args: 参数 ...表示可变参数
*/
return (std::forward<Func>(f)) (std::forward<Args>(args)...);
}
int f(int a, char b){
return a + b;
}
int main(){
std::cout << myMap(f, 2, 'c');
system("pause");
return 0;
}
输出:
101