左值引用只能绑定左值,右值引用只能绑定右值,常量左值可以绑定一切:左值、右值、常量左值和常量右值。
universal reference:需要自动推导的类型
有两种情况,一个是模板的自动推导(只有T&&
这种格式才是,其外加一点点都不行),一个是auto的自动推导
template
void f(T&& param);
f(10);///<模板中的param是右值,因为10是纯右值
int x = 10;
f(x);///<模板中的param是左值,因为x是左值
template
class Test
{
Test(Test&& param);///<不是universal reference,因为没有类型推导,是右值引用
}
void f(std::vector&& pram);//不是universal reference,因为在调用这个函数之前vector已经确定了。
template
void f(const T&& param);///<不是universal reference,因为加了const
上一节讲了universal reference,类型推导的时候,如果有多种类型配合在一起会怎么样,这就是引用折叠:
左值和右值是独立于它们的类型,右值引用类型可能是左值也可能是右值。具名的右值引用是左值,未命名的右值引用是右值
int&& var1 = 0; ///
再来一个例子,证明具名右值和非具名右值,传入forward的是右值引用,但是变成了x就是具名的了,它就变成了左值
void P(int& x)
{
cout<<"lvalue"<
这个用于处理“具名的右值引用是左值”,本来forward函数中的x在P(x)
是左值了,所以第一个打印的应该左值,被std::forard()
修饰后的类型恢复了其本身的类型——左值,因此它的类型是右值。
void P(int& x) { cout << "lvalue" << endl; }
void P(int&& x) { cout << "rvalue" << endl; }
void forward(int&& x) {
P(x);
P(std::forward(x));
}
int main() {
forward(1);
return 0;
}
C++11将右值分为常量右值和将亡值,常量右值就是数字啥的,将亡值指的如果函数有返回值,那个返回值就是。
int fun(void)
{
int x=1;
return x;
}
int main(void)
{
int x=0;
int& y = x;///<左值引用
int&& z = fun();///<将亡值的右值引用
int&& a = 0;///<常量右值的右值引用
const int& X = x;///<常量左值可以接受左值引用
const int& Z = z;///<常量左值可以接受右值引用
return 0;
}
返回局部变量的值:
1.局部变量是手动创建的:RVO,没有任何消耗
ff::five xxx()
{
ff::five b("aac");
return b;
}
int main() {
ff::five a("bbc");
std::cout<
4
2.局部变量是外面构造传进来:拷贝构造
ff::five xxx(ff::five b)
{
return b;
}
int main() {
ff::five a("bbc");
std::cout<
copy constructor
move constructor
4
返回全局变量的值:会调用拷贝构造
ff::five c("bbc");
ff::five xxx()
{
return c;
}
int main() {
ff::five a("bbc");
std::cout<
copy constructor
4
返回全局变量的引用:不会有消耗
ff::five c("bbc");
ff::five& xxx()
{
return c;
}
int main() {
ff::five a("bbc");
std::cout<
4
返回局部变量的引用:报错,很简单,不能返回局部变量的指针
ff::five& xxx(ff::five c)
{
return c;
}
int main() {
ff::five a("bbc");
std::cout<
如果函数内部有new就要建5个函数:析构、拷贝构造、拷贝赋值、移动构造和移动赋值。
下面是我认为对的,这里有很多要注意的。
=
运算符,而且等号左侧的这个对象肯定是已经存在的,用新的值覆盖掉,因此原有的参数必须安全清空,赋值肯定是比拷贝快的#include
#include
using namespace std;
class five {
public:
char* str_;
five(const char* tmp) {
if (nullptr != tmp) {
size_t n = strlen(tmp) + 1;
str_ = new char(n);
memcpy(str_, tmp, n - 1);
str_[n - 1] = '\0';
}
cout << "constructor" << endl;
}
~five() {
deconstructor();
}
five(const five& other) //拷贝构造
{
if (nullptr != other.str_) {
size_t n = strlen(other.str_) + 1;
str_ = new char(n);
memcpy(str_, other.str_, n - 1);
str_[n - 1] = '\0';
}
cout << "copy constructor" << endl;
}
five& operator=(const five& other) //<拷贝赋值
{
if (nullptr != other.str_) {
size_t n = strlen(other.str_) + 1;
deconstructor();
str_ = new char(n);
memcpy(str_, other.str_, n - 1);
str_[n - 1] = '\0';
}
cout << "copy assignment" << endl;
return *this;
}
five(five&& other) //<移动构造
{
str_ = other.str_;
other.str_ = nullptr;
cout << "move constructor" << endl;
}
five& operator=(five&& other) //<移动赋值
{
deconstructor();
str_ = other.str_;
other.str_ = nullptr;
cout << "move assignment" << endl;
return *this;
}
private:
void deconstructor()
{
delete str_;
str_ = nullptr;
}
};
上面会有很多if(other.str_==nullptr)
的判断,这种判断大多数明显是无效的,能不能省掉呢?
下面会通过vector扩容讲拷贝构造和移动构造的关系。总的来说,因为new可能会失败,第一种办法没法标注noexcept。
下面链接中的说法感觉不是很好,它说的是:移动构造中,如果先析构this->str_
再构造新的数据,万一new失败了,析构的也都丢了。所以只能先构造再析构,但这种方法会造成第三个缺点。
这个确实是,上面代码有很多重复的地方,虽然可以通过抽出函数的方法来解决,但是我能不能换一种思路呢?不按照原本四个函数的功能去实现,就是这里的办法。
上面的三个理由,感觉都没有那么强烈,只有我自己得出的那个理由比较靠谱。
https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
#include
namespace ff {
class five {
public:
char* str_;
size_t size_;
five() {
str_ = nullptr;
size_ = 0;
}
five(const char* str) : str_(nullptr) {
if (nullptr != str) {
size_ = strlen(str) + 1;
str_ = new char(size_);
memcpy(str_, str, size_);
}
// cout<<"constructor"<
准确的说这个叫rule of four and a-half,因为里面不是五个函数,而是“四个半”。
析构函数:普通的
拷贝构造:没啥特殊的,和普通的构造一毛一样。
移动构造:这里用了exchange函数,这个函数的作用就是将第一个参数的结果返回,第二个参数的结果赋给第一个参数。
赋值函数:重载了赋值运算符,参数必须只能是值,不能是左值或右值引用
swap函数:这个就是那半个参数必须只能是左值引用
此外还可以添加一个友元的swap函数
这样是为了让这个函数变成noexcept的,所有的构造发生在函数外。如果是左值,那就会调用拷贝构造生成,进来赋值,如果传进来的是右值,那就会调用移动构造生成再赋值。
再来一个问题,右值引用和左值引用和值是三种不同的类型,现象就是三者可以进行重载。那问题就来了,既然是三种类型,值应该不支持左值和右值传入的才对啊,应该会提示函数不存在啊,为什么能过呢?
首先值传递在汇编阶段会生成两个函数,一个是值的一个是右值的,就是说一个函数会变成两个(这个我没看懂,是同事看的汇编)。那为什么要这么做呢?大帝的说法是右值引用可以绑定值。或者可以猜测为是历史原因。例如一个函数void func(int x)
,如果你在调用的时候完全可以写为func(3);
,但是这个3是纯右值啊,凭什么可以给值赋值啊,应该有这样的函数才对void func(int&& x);
,但这是历史问题,C开始就一直这么做,现在发明了右值引用,不允许我这么传参数了?凭啥?就类似的原因。这个3是因为编译器会生成一个临时变量a,用3给a赋值,这样就可以了。只不过这个操作会在后续被编译器优化掉,3会直接放在寄存器中。
上面这段有主管臆测的部分,没有绝对的证据证明。
如果是拷贝赋值,最好使用左值引用当参数,如果是移动赋值应该是右值引用啊,那为啥这里的重载用的却是值呢?
如果是移动赋值,肯定还是要用赋值函数,但是参数不对,因此就会先在本地调用移动构造,将右值引用的值移动到一个临时变量,再把这个变量传入赋值函数(用值传入,不会有问题吗?)总的来说就是移动赋值需要先调用移动构造,再调用赋值函数。同理,拷贝赋值就是先调用拷贝构造再调用赋值函数。
赋值函数中调用的copy.swap()这个成员函数,而不是用的std::swap(copy, *this)
,因为这会造成循环引用。
swap的原理大概是下面这个。std::swap()是利用的重载赋值运算符,而重载出来的赋值运算符,再次调用std::swap(),这样就死循环了。
template void swap (T&a, T&b)
{
T c(std::move(a)); a=std::move(b); b=std::move(c);
}
这个是因为ADL。ADL的原理是虽然某个函数在调用的时候没有声明namespace,但是如果全部参数都属于同一个namespace,就会调用那个namespace的函数。例如swap的两个参数都是std::vector,在调用swap的时候不需要声明。
std::vectora1,a2;
std::swap(a1, a2);///<这个std可以不加
swap(a1, a2);
而友元的作用很像成员函数,但是它的作用域并不属于类。例如上面swap如果不用友元就要声明一个类内的swap函数,完了再在namespace中,类外部分再写一个swap函数。因为ADL中经常有人用swap(a, b)
这种写法,要是只用成员函数就只能a.swap(b)
。
大帝曾经曰过:要尽量多的减少成员函数和friend的个数,因为这样会破坏类的封装性,主要的原因是减少访问私有变量的次数,例如任何容器都没有swap函数这个成员,都是放在单独的algorithm中。反正类内和类外都要有个swap,莫不如用friend一次性解决。
这个非常重要,noexcept的原理是告诉别人我这个函数非常稳定,不可能会出错。如果不加,那就是可能会出错。一般而言,只有绝对不会出现问题的函数才会被加这个标记。
那这个标记有啥用呢?我目前发现了一个非常重要的情况,如果vector不出现扩容,push_back的类型如果是值,那自然会调用拷贝构造,如果传入的是右值的,那必然是移动构造,这都没问题,但是如果vector扩容呢?问题就来了,如果加了noexcept的,会调用拷贝构造,如果没加的就会调用移动构造。这样效率就会差很多了。
这里移动相关的都可以加noexcept,因为它们就是不会出错,最大可能出错的是new,而new就算失败也是传进来之前就出错了,而不是函数本身异常。
int main() {
ff::five a("bbc");
std::vector b;
b.push_back(a);
std::cout << b.capacity() << std::endl;
b.push_back(a);
std::cout << b.capacity() << std::endl;
b.push_back(a);
std::cout << b.capacity() << std::endl;
return 0;
}
下面是移动构造加了noexcept的,很明显,1->2移动一个,2->4移动两个
copy constructor
1
copy constructor
move constructor
2
copy constructor
move constructor
move constructor
4
下面是没加noexcept的,看到没,竟然还用的拷贝构造,天呐!!!!原来vector扩容是可以没有消耗的,怪不得大帝说没有删除的情况,vector是最好用的数据结构了。
copy constructor
1
copy constructor
copy constructor
2
copy constructor
copy constructor
copy constructor
4
https://stackoverflow.com/questions/5695548/public-friend-swap-member-function
当临时变量时,右值引用可以延长这个变量的生命周期,省去复制的操作,但是很多时候根本用不到这个。例如函数返回一个对象,这个对象是局部变量的临时值,右值引用还用不了,那要怎么做才能减少复制呢?下面代码rule_of_five是cppreference的,我只是加了些打印
#include
using namespace std;
class rule_of_five
{
public:
char* cstring; // raw pointer used as a handle to a dynamically-allocated memory block
rule_of_five(const char* s = "") : cstring(nullptr)
{
if (s)
{
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // allocate
std::memcpy(cstring, s, n); // populate
}
std::cout<<"constructor"<
$ g++ rule_of_five.cpp -Wall -g
$ ./a.out
constructor
what the hell?
what the hell?
deconstruct
有意思吧,竟然只创造了一次对象?是真的,就是只创造了一次,那这是怎么做到的呢?这个就很有意思了,编译器竟然把return_five()
中局部变量的值直接赋值出来了,如果打印地址就会发现,这个a的地址竟然都和函数内的f一样,有没有很帅。
不过这个RVO是可以被关掉的,比如之前为了仔细看到移动构造的使用在编译的时候增加-fno-elide-constructors
编译选项,可见结果就不一样了。
constructor
move constructor
what the hell?
move constructor
move constructor
what the hell?
deconstuct
这样的结果是很好理解的,创建好临时变量后,通过移动构造给到f,打印好f的结果后返回,又通过移动构造,把结果给到a。
那如果删掉移动构造呢?那就会启用拷贝构造来代替,就会不停地构造析构。
constructor
constructor
copy constructor
deconstuct
what the hell?
constructor
copy constructor
deconstuct
constructor
copy constructor
deconstuct
what the hell?
deconstuct
如果返回函数内部的参数内容,返回内部的引用就可以,注意不要返回局部栈变量。此外我还问过大帝返回右值引用的问题,他说:为啥要返回右值引用,这个需求就很奇怪。