【C++11】右值引用和移动语义 万能引用和完美转发(二)

简介: 【C++11】右值引用和移动语义 万能引用和完美转发(二)
int main()
{
  grm::string ret1;
  ret1= grm::to_string(1234);
  return 0;
}

4c8697b1e8854c24a92dc160801e1ef5.png我们可以知道:将局部对象的资源转移给了临时对象,然后将临时对象拷贝赋值给ret1,但是为啥这里打印了拷贝构造和拷贝赋值啊?别忘了,我们用的是现代写法:拷贝赋值是借助拷贝构造实现的。

那当我们实现了移动赋值后呢?

// 移动赋值
    string& operator=(string&& s)
    {
      cout << "string& operator=(string&& s) -- 移动语义" << endl;
      swap(s);
      return *this;
    }

我们再次运行:

29443ded2ec04379935183811ef14b78.png

这里依旧是先将局部对象str的资源先转移给临时对象然后再转移给ret1

C++11后,STL的所有容器都增加了右值引用的版本:

2a9a37a3e1ea4372a75985a0b24b3e78.png

STL容器中,在insert等成员方法中也增加了右值引用:


99004e93a89a485fb54200cb15979ac5.png

其实也很好理解,当我们插入一个临时对象(接就是将亡值的时候)会直接调用右值引用的版本将将亡值的资源给转移走。

所以我们可以总结出:其实左值引用和右值引用本质上都是减少拷贝,提高效率。但是他们出发的角度不同:左值引用是直接从拷贝出发的,右值引用是可以转移一些将亡值的资源。

有些书上面写着,右值引用可以延长对象的生命周期,其实我觉得不太准确,因为对象的生命周期其实并没有延长,延长的是对象里面资源的生命周期。

1.3 新的类功能

原来C++类中,有6个默认成员函数:

构造函数

析构函数

拷贝构造函数

拷贝赋值重载

取地址重载

const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

如果大家想强制生成默认函数可以用default关键字,强制不生成默认函数可以用关键字delete

2 万能引用与完美转发

2.1 万能引用的基本介绍和使用

首先我们先来看看下面的代码:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
  Fun(t);
}
int main()
{
  PerfectForward(10);
  int a;
  PerfectForward(a); 
  PerfectForward(std::move(a)); 
  const int b = 8;
  PerfectForward(b); 
  PerfectForward(std::move(b)); 
  return 0;
}

我们来看看运行结果:

d3d6d93139ec4d40a4ac522a6292c3ad.png

怎么结果全是左值引用呀❓我们不是也引用了右值的吗❓

其实模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。(有些地方也叫做引用折叠)但是在后续使用中都退化成了左值。

所以我们刚才看见的打印的全部是左值。

假如我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。

2.2 完美转发

std::forward<T>(t)   //在传参的过程中保持了t的原生类型属性。

比如上面的代码我们可以改为:

void PerfectForward(T&& t)
{
  Fun(forward<T>(t));
}

当我们再次运行时:


e1640a14d7cf455fb92785cdff1a5624.png

这样就保留了上层传入对象的类型。

2.3 完美转发的实际应用场景

我们可以借用下当初模拟实现list的那部分代码:凡是向下传的参数都得forward,

namespace grm
{
  template<class T>
  struct ListNode
  {
    ListNode* _next = nullptr;
    ListNode* _prev = nullptr;
    T _data;
    ListNode(const T& data=T())
      :_next(nullptr)
      ,_prev(nullptr)
      ,_data(data)
    {}
    ListNode(T&& data )
      :_next(nullptr)
      , _prev(nullptr)
      , _data(std::forward<T>(data))
    {}
  };
  template<class T>
  class List
  {
    typedef ListNode<T> Node;
  public:
    List()
    {
      _head = new Node;
      _head->_next = _head;
      _head->_prev = _head;
    }
    void PushBack(T&& x)
    {
      //Insert(_head, x);
      Insert(_head, std::forward<T>(x));
    }
    void PushFront(T&& x)
    {
      //Insert(_head->_next, x);
      Insert(_head->_next, std::forward<T>(x));
    }
    void Insert(Node* pos, T&& x)
    {
      Node* prev = pos->_prev;
      Node* newnode = new Node(std::forward<T>(x));
      prev->_next = newnode;
      newnode->_prev = prev;
      newnode->_next = pos;
      pos->_prev = newnode;
    }
    void Insert(Node* pos, const T& x)
    {
      Node* prev = pos->_prev;
      Node* newnode = new Node(x);
      prev->_next = newnode;
      newnode->_prev = prev;
      newnode->_next = pos;
      pos->_prev = newnode;
    }
  private:
    Node* _head;
  };
}
int main()
{
  grm::List<grm::string> lt;
  lt.PushBack("1111");
  lt.PushFront("2222");
  return 0;
}

注意上面代码是在模拟实现list的基础上改编了一下,否则太长了不太好看。不过总体思路都是一样的,凡是我们向下传的参数都得完美转发一下


3 可变参数模板

3.1基本语法

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。

下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

3.2递归函数方式展开参数包

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
 cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
 cout << value <<" ";
 ShowList(args...);
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

我们如果想打印参数包大小语法形式是这样的:cout << sizeof...(args) << endl;

3.3逗号表达式展开参数包

template <class T>
void PrintArg(T t)
{
 cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
 int arr[] = { (PrintArg(args), 0)... };
 cout << endl;
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

3.4 可变参数模板的应用

那究竟STL在哪些地方用到了可变参数模板呢?

首先我们来看看C++11系列提供的emplace系列。

我以vector为例:

8790ddb1062e4d76b3262f1225b9c827.png

7ac1f1fe03a34c5b9d5f773e7f8f3f21.png

网上有人说使用emplace_back系列的效率是高于push_back系列的,但是其实这种说法是不够全面的,我们下面来分析分析:

如果插入的数据是有资源的右值(比如插入了一个string的匿名对象),调用push_back的话是一次构造+移动构造,调用emplace_back的话直接是一次构造,总的来说差别不大。但是如果插入的像日期类的对象,调用push_back的话是一次构造+拷贝构造,调用emplace_back的话直接是一次构造,这样来看效率的确会高一些。所以我们得分情况讨论。

不仅在emplace系列,在C++11提供的线程库中对可变参数模板也有应用,我们先来简单的看看,具体的讲解我将会在讲解线程库的博文中讲解。

c8850d6440a64cfc89832338519d96d7.png

好了,今天的讲解就到这里了,如果觉得该文章对你有帮助的话能不能3连支持一下。😘😘😘

目录
相关文章
|
2月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
23 3
|
2月前
|
存储 自然语言处理 编译器
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
|
3月前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
53 5
|
3月前
|
C++
C++引用
C++引用
|
2月前
|
C++
C++基础知识(二:引用和new delete)
引用是C++中的一种复合类型,它是某个已存在变量的别名,也就是说引用不是独立的实体,它只是为已存在的变量取了一个新名字。一旦引用被初始化为某个变量,就不能改变引用到另一个变量。引用的主要用途包括函数参数传递、操作符重载等,它可以避免复制大对象的开销,并且使得代码更加直观易读。
|
2月前
|
存储 自然语言处理 编译器
|
2月前
|
安全 C++
|
3月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
31 0
|
1天前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
41 30