上节我们提出了右值引用,可以用来区分右值,那么这有什么用处?
问题来源
我们先看一个C++中被人诟病已久的问题:
我把某文件的内容读取到vector中,用函数如何封装?
大部分人的做法是:
void readFile(const string &filename, vector<string> &words)
{
words.clear();
//read XXXXX
}
这种做法完全可行,但是代码风格谈不上美观,我们尝试着这样编写:
vector<string> readFile(const string &filename)
{
vector<string> ret;
ret.push("cesfwfgw"); //....
// return ret;
}
这样我们就可以在main中这样调用:
vector<string> coll = readFile("fef.text");
但是,稍微熟悉C++的都知道,这样在语法上会造成大量的开销:
这中间产生两次复制和销毁的开销。
如果说这个例子,可以采用开头的代码解决开销,那么如果是一个查询返回结果的函数,那么我们必须这样写:
vector<string> queryWord(const string &word)
{
vector<string> result;
//XXXXX return result;
}
这里的开销就无法避免了。
移动语义的引入
我们考虑一个生活中常见的问题(这里参考了如何评价 C++11 的右值引用(Rvalue reference)特性?),如果把一个很重的货物从箱子A移动到箱子B,那么
事实上,C++98采用的就是最后一种效率极其低下的做法,这里的关键在于,C++98没有很好的区分“copy”和“move”语义。
上述问题中,我们明确提出移动A到B中,但是C++98由于移动语义的缺失,只好采用copy的方式完成。
我们再回到开头的问题中:
vector<string> readFile(const string &filename)
{
vector<string> ret;
ret.push("cesfwfgw"); //....
// return ret;
}
这里我们必须看到一点,在完成函数调用后,ret就要被销毁,所以我们想到一个主意,不是把ret中的内容复制给main中的coll,而是将ret中的内容偷到coll中,然后将ret悄悄的销毁。
这样是可行的,因为ret的生命周期很短。
哪些可以偷?
现在问题来了,C++中哪些可以偷,哪些不能?
我们回顾上一节提到的四个表达式:
string one("one");
const string two("two");
string three() { return "three"; }
const string four() { return "four"; }
显然,one和two生命周期较长,不能偷。four具有const属性,拒绝被偷。
那么three是可以被偷取的,因为它是临时变量,又没有const属性。
所以,C++中的非const右值,和移动语义完全匹配。
上节我们提出用右值引用区分右值,正是为了解决哪些可以偷的问题!
OK,我们的思路已经很清晰了:
这就是右值引用的来源。
如果一个变量不是右值,但是我们又需要偷取,那么我们可以采用std::move函数,将其强制转化为右值引用。
例如:
void test(string name)
{
string temp(std::move(name));
// XXXXXX
}
注意,被偷取之后的name无法继续使用,所以move函数不可以随意使用。
带来的影响
那么,右值引用带来哪些该变呢?
首先是类的成员函数赋值,看下面代码:
class People
{
public:
People()
{
cout << "People()" << endl;
}
People(const string &name)
: name_(name)
{
cout << "People(const string &name)" << endl;
}
People(string &&name)
: name_(name)
{
cout << "People(string &&name)" << endl;
} private:
string name_;
};
这里name赋值,我们相对于C++98,提供了一个右值函数,将name的值移动给name_。
事实上,上面的两个函数可以合成一个:
class People
{
public:
People()
{
cout << "People()" << endl;
} People(string name)
: name_(std::move(name))
{
cout << "People(string name)" << endl;
} private:
string name_;
};
这里注意,上面的name采用传值,并没有带来开销,因为:
对于构造函数,除了提供复制构造函数,还需要移动构造函数。如下:
class People
{
public:
People()
{
cout << "People()" << endl;
} People(string name)
: name_(std::move(name))
{
cout << "People(string name)" << endl;
} People(const People &p)
: name_(p.name_)
{
cout << "People(const People &p)" << endl;
}
People(People &&p)
: name_(std::move(p.name_))
{
cout << "People(People &&p)" << endl;
} private:
string name_;
};
注意在最后一个People(People &&p)中,移动p内的name时,必须显式使用move,来强制移动name成员。
同样,还有移动赋值运算符。
另外,在C++98中,容器内的元素必须具备值语义,现在则不同,元素具备移动能力即可,后文我们在智能指针系列会提到unique_ptr,它可以放入vector中,但是不具有复制和赋值能力。
其他的影响请参考:如何评价 C++11 的右值引用(Rvalue reference)特性?
下文通过一个string的模拟实现,演示右值引用的使用。