分析在各种情况下具体调用的是下面几种的哪一种:
默认构造函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动赋值函数
#include<iostream>
#include<vector>
#include<string>
using namespace std;
class Test
{
public:
Test(const string& s = "hello world") :str(new string(s)) { cout << "默认构造函数" << endl; };
Test(const Test& t);
Test& operator=(const Test& t);
Test(Test&& t) noexcept;
Test& operator=(Test&& t) noexcept;
~Test();
public:
string* str;
};
Test::Test(const Test& t)
{
str = new string(*(t.str));
cout << "拷贝构造函数" << endl;
}
Test& Test::operator=(const Test& t)
{
cout << "拷贝赋值函数" << endl;
return *this;
}
Test::Test(Test&& t)noexcept
{
str = t.str;
t.str = nullptr;
cout << "移动构造函数" << endl;
}
Test& Test::operator=(Test&& t)noexcept
{
cout << "移动赋值函数" << endl;
return *this;
}
Test::~Test()
{
cout << "析构函数" << endl;
}
int main()
{
Test str0 = Test("hello"); // 默认构造函数
Test str1("hello"); //默认构造函数
Test str2 = str1; //拷贝构造函数
Test str3(str1); //拷贝构造函数
Test str4; //默认构造函数
str4 = str3; //拷贝赋值函数
Test&& str5 = std::move(str0); //右值引用,不调用任何构造函数
Test str6 = str5; //拷贝构造函数,str5为右值引用,不等于右值。
// 右值引用在作为表达式时是左值!
// 具体看下面的解释
Test str7 = std::move(str1); //移动构造函数
Test str8(str7); //拷贝构造函数
Test str9; //默认构造函数
str9 = std::move(str2); //移动赋值函数
Test& str10 = str3; //左值引用别名,不调用任何构造函数
vector<Test> vec(1); //默认构造函数
Test t("hello"); //默认构造函数
vec.push_back(std::move(t)); //移动构造函数调用两次
//第一次移动构造函数是因为vec的size==capacity,重新分配内存,移动构造过去。
//第二次移动构造函数是因为std::move。
return 0;
}
为什么str5是左值:
原因:
https://en.cppreference.com/w/cpp/language/reference 中提到
rvalue reference variables are lvalues when used in expressions
为什么C++的右值引用&&传参时会看做左值呢,具体看这个链接:
https://www.zhihu.com/question/481479365
1.值类型是关于表达式(expression)的分类。右值引用、左值引用是对象的分类。右值引用不等于右值!!
2.左值可以叫做有址,右值可以叫无址。左值是指向特定内存的东西,右值是不指向任何地方的东西,左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。
3.右值引用的作用
相当于右值引用使得暂存便永久了,C++11为了使用移动构造函数,所以定义了右值引用
所以右值引用作为变量表达式都为左值。
左值和右值
右值引用,用以引用一个右值,可以延长右值的生命期,比如:
int&& i = 123;
int&& j = std::move(i);
int&& k = i;//编译不过,这里i是一个左值,右值引用只能引用右值
可以通过下面的代码,更深入的体会左值引用和右值引用的区别:
int i;
int&& j = i++;
int&& k = ++i;
int& m = i++;
int& l = ++i;
move.cpp: In function ‘int main()’:
move.cpp:72:14: error: cannot bind ‘int’ lvalue to ‘int&&’
int&& k = ++i;
^
move.cpp:73:15: error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
int& m = i++;
此外,常量字符串“abc”这种也是左值。
std::move使用场景
1 定义的类使用了资源并定义了移动构造函数和移动赋值运算符,否则用了std::move也为拷贝构造。
2 该变量即将不再使用
3 不能move一个const对象,否则还是会拷贝link
https://zhuanlan.zhihu.com/p/94588204
std::forward完美转发
引用折叠
(一)引用折叠
-
在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意两个&之间有空格)这种直接定义引用的引用是不合法的,但是编译器在通过类型别名或模板参数推导等语境中,会间接定义出“引用的引用”,这时引用会形成“折叠”。
-
引用折叠会发生在模板实例化、auto类型推导、创建和运用typedef和别名声明、以及decltype语境中。
(二)引用折叠规则
- 两条规则
(1)所有右值引用折叠到右值引用上仍然是一个右值引用。如X&& &&折叠为X&&。
(2)所有的其他引用类型之间的折叠都将变成左值引用。如X& &, X& &&, X&& &折叠为X&。可见左值引用会传染,沾上一个左值引用就变左值引用了。根本原因:在一处声明为左值,就说明该对象为持久对象,编译器就必须保证此对象可靠(左值)。 - 利用引用折叠进行万能引用初始化类型推导
(1)当万能引用(T&& param)绑定到左值时,由于万能引用也是一个引用,而左值只能绑定到左值引用。因此,T会被推导为T&类型。从而param的类型为T& &&,引用折叠后的类型为T&。
(2)当万能引用(T&& param)绑定到右值时,同理,右值只能绑定到右值引用上,故T会被推导为T类型。从而param的类型就是T&&(右值引用)。
std::forward与完美转发
定义
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept{
// forward an lvalue as either an lvalue or an rvalue
return (static_cast<T&&>(arg));
}
左值转左值,右值转右值。
通常要结合万能引用&&一起使用
完美转发:
template<typename T>
void testForward(T&& param)
{
//不完美转发
print(param); //param为形参,是左值。调用void print(const int& t)
print(std::move(param)); //转为右值。调用void print(int&& t)
//完美转发
print(std::forward<T>(param)); //只有这里才会根据传入param的实参类型的左右值进转发
}
完美转发失败
1. 完美转发不仅转发对象,还转发其类型、左右值特征以及是否带有const或volation等修饰词。而完美转发的失败,主要源于模板类型推导失败或推导的结果是错误的类型。
2. 实例说明:假设转发的目标函数f,而转发函数为fwd(天然就应该是泛型)。函数如下:
template<typename… Ts>
void fwd(Ts&&… params)
{
f(std::forward<Ts>(params)…);
}
f(expression); //如果本语句执行了某操作
fwd(expression); //而用同一实参调用fwd则会执行不同操作,则称完美转发失败。