C++ 拷贝/移动构造 std::move std::forward

分析在各种情况下具体调用的是下面几种的哪一种:
默认构造函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动赋值函数

#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完美转发

引用折叠

(一)引用折叠

  1. 在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意两个&之间有空格)这种直接定义引用的引用是不合法的,但是编译器在通过类型别名或模板参数推导等语境中,会间接定义出“引用的引用”,这时引用会形成“折叠”。

  2. 引用折叠会发生在模板实例化、auto类型推导、创建和运用typedef和别名声明、以及decltype语境中。

(二)引用折叠规则

  1. 两条规则
    (1)所有右值引用折叠到右值引用上仍然是一个右值引用。如X&& &&折叠为X&&。
    (2)所有的其他引用类型之间的折叠都将变成左值引用。如X& &, X& &&, X&& &折叠为X&。可见左值引用会传染,沾上一个左值引用就变左值引用了。根本原因:在一处声明为左值,就说明该对象为持久对象,编译器就必须保证此对象可靠(左值)。
  2. 利用引用折叠进行万能引用初始化类型推导
    (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则会执行不同操作,则称完美转发失败。

具体链接参考:
https://www.cnblogs.com/5iedu/p/11324772.html

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Contents
滚动至顶部