【总结】【C++11】Move&forward&右值

预备知识:

https://blog.csdn.net/lisemi/article/details/100775218

下文先从C++11引入的几个规则,如引用折叠右值引用的特殊类型推断规则、static_cast的扩展功能说起,然后通过例子解析std::move和std::forward的推导解析过程,说明std::move和std::forward本质就是一个转换函数,std::move执行到右值的无条件转换,std::forward执行到右值的有条件转换在参数都是右值时二者就是等价的。其实std::move和std::forward就是在C++11基本规则之上封装的语法。

规则3:虽然不能隐式的将一个左值转换为右值引用,但是可以通过static_cast显示地将一个左值转换为一个右值。【C++11中为static_cast新增的转换功能】。

引用折叠:

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

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

引用折叠规则:(根本原因:在一处声明为左值,就说明该对象为持久对象,编译器就必须保证此对象可靠(左值))
X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&函数模板参数推导规则(右值引用参数部分):
当函数模板的模板参数为T而函数形参为T&&(右值引用)时适用本规则。
若实参为左值 U& ,则模板参数 T 应推导为引用类型 U& 。
(根据引用折叠规则, U& + && => U&, 而T&& ≡ U&,故T ≡ U& )
若实参为右值 U&& ,则模板参数 T 应推导为非引用类型 U 。
(根据引用折叠规则, U或U&& + && => U&&, 而T&& ≡ U&&,故T ≡ U或U&&,这里强制规定T ≡ U )std::remove_reference为C++0x标准库中的元函数,其功能为去除类型中的引用。
std::remove_reference<U&>::type ≡ U
std::remove_reference<U&&>::type ≡ U
std::remove_reference<U>::type ≡ U以下语法形式将把表达式 t 转换为T类型的右值(准确的说是无名右值引用,是右值的一种)
static_cast<T&&>(t)无名的右值引用是右值
具名的右值引用是左值。注:本文中 ≡ 含义为“即,等价于“。

 

※重点混淆点:

1.默认移动构造函数

https://blog.csdn.net/shenwanjiang111/article/details/53576196(类的特殊默认函数)

这里写图片描述
move语义:如果对象有移动构造,就优先调用移动构造,如果没有移动构造,就调用拷贝构造

a.c++ Primer:只有当一个类没有定义任何自己的拷贝控制函数,并且其所有数据成员都可以移动构造或移动赋值时,编译器会为它合成移动构造函数.........................与拷贝操作不同,编译器根本不会为某些类合成移动操作。特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。.... ~嗯,这样看来合理,指针所指向的资源不需要析构(也就没定义析构函数),那就默认生成移动构造函数也没问题

class A 
{ 
public: int *p = new int; 
string str = "Fuck"; 
};
 int main()
 { A a; A b(std::move(a));
 cout << a.p << " " << a.str << " ";
 cout << b.p << " "<<b.str << " ";
 system("pause"); 
}

b.个人理解:C++编译器合成的默认移动函数对于基本类型所谓移动只是把其值拷贝,对于string这类类成员中因为其类实现了移动构造函数才会真正的所谓资源移动。
另外只要定义了析构函数,那就不会默认生成移动构造函数,因为对于基本类型来说是"复制"而非真正的移动,只要其一析构了,另外一个的资源也就没了.所以只要定义了析构函数,就不会合成移动构造~(刚刚测试了,定义了析构,结果输出了2个fuck,确实是这样)

(就如你说的,当一个类里的成员只有POD类型的时候,其实移动构造和拷贝构造表现上都一样,你说是复制也好移动也行,并没区别。如果你的类中包含了non-POD成员,只有当所有non-POD都有移动构造函数时,才会合成默认移动构造函数(内部就是依次调用所有成员的移动构造函数),否则会依次调用所有成员的复制构造函数。若是某个成员定义了析构函数,就不属于POD类型了,自然是不会合成默认移动构造函数了。)

 

2.如果需要传递未知的引用(包含右值引用),传递的形参需要是右值引用(&&),根据折叠规则,右值进来为右值,左值进来为左值。达到传递右值和左值引用的目的!    然后可以对传进来的参数进行std::move转换成右值!(可参考笔记C++ Core Guidelines)   ;;也可自己创建类的move赋值构造和move拷贝构造函数

std::unique_ptr<String>     m_string;
void construct(std::unique_ptr<String>&& string )
{
    m_tring(std::move(string));   //此时,无论左值右值,都会转换成右值,然后调用unique_ptr中的move构造函数
}

 

 

 

源码分析:

https://blog.csdn.net/zwvista/article/details/6848582

https://blog.csdn.net/p942005405/article/details/84644069  (详细讲解std::)

左值右值及Move,forward:

https://blog.csdn.net/xiaolewennofollow/article/details/52559306

定义:

1、左值和右值的概念

         左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
         右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
         一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址

2、引用

        引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。

        引用可以改变指针的指向,还可以改变指针所指向的值。

        引用的基本规则:

  1. 声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义
  2. 对引用的一切操作,就相当于对原对象的操作。

3、左值引用和右值引用

    3.1 左值引用
         左值引用的基本语法:type &引用名 = 左值表达式;

    3.2 右值引用

        右值引用的基本语法type &&引用名 = 右值表达式;

        右值引用在企业开发人员在代码优化方面会经常用到。

        右值引用的“&&”中间不可以有空格。

 

左值就是有名字的变量(对象),可以被赋值,可以在多条语句中使用,而右值呢,就是临时变量(对象),没有名字,只能在一条语句中出现,不能被赋值。

右值引用(c11新增了很多如下的&&重载,也就是move性能)

左值的声明符号为”&”, 为了和左值区分,右值的声明符号为”&&”

例1:vector的push_back

void push_back (const value_type& val);

void push_back (value_type&& val);

例2:unique_ptr等移动构造函数

http://www.cplusplus.com/reference/memory/unique_ptr/unique_ptr/

move (6)
unique_ptr (unique_ptr&& x) noexcept;
move-cast (7)
template <class U, class E>
  unique_ptr (unique_ptr<U,E>&& x) noexcept;

下面涉及到一个问题: 
x的类型是右值引用,指向一个右值,但x本身是左值还是右值呢?C++11对此做出了区分:


  Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

x是有名字的,所以x在这里被视为一个左值(可以通过forward避免此情况,在下面forward例中有例子)

右值引用的意义

直观意义:为临时变量续命,也就是为右值续命,因为右值在表达式结束后就消亡了,如果想继续使用右值,那就会动用昂贵的拷贝构造函数。(关于这部分,推荐一本书《深入理解C++11》) 
右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。 
转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。 
通过转移语义,临时对象中的资源能够转移其它的对象里。 
在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。 (以下有例子)
普通的函数和操作符也可以利用右值引用操作符实现转移语义
 

 class MyString { 
 private: 
  char* _data; 
  size_t   _len; 
  void _init_data(const char *s) { 
    _data = new char[_len+1]; 
    memcpy(_data, s, _len); 
    _data[_len] = '\0'; 
  } 
 public: 
  MyString() { 
    _data = NULL; 
    _len = 0; 
  } 

  MyString(const char* p) { 
    _len = strlen (p); 
    _init_data(p); 
  } 

  MyString(const MyString& str) { 
    _len = str._len; 
    _init_data(str._data); 
    std::cout << "Copy Constructor is called! source: " << str._data << std::endl; 
  } 

  MyString& operator=(const MyString& str) { 
    if (this != &str) { 
      _len = str._len; 
      _init_data(str._data); 
    } 
    std::cout << "Copy Assignment is called! source: " << str._data << std::endl; 
    return *this; 
  } 

  virtual ~MyString() { 
    if (_data) free(_data); 
  } 
 }; 

 int main() { 
  MyString a; 
  a = MyString("Hello"); 
  std::vector<MyString> vec; 
  vec.push_back(MyString("World")); 
 }

此时,调用的是拷贝构造函数和复制构造函数:

 Copy Assignment is called! source: Hello 
 Copy Constructor is called! source: World

这个 string 类已经基本满足我们演示的需要。在 main 函数中,实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

我们先定义转移构造函数。
 

  MyString(MyString&& str) { 
    std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
    _len = str._len; 
    _data = str._data; 
    str._len = 0; 
    str._data = NULL; 
 }

有下面几点需要对照代码注意: 
1. 参数(右值)的符号必须是右值引用符号,即“&&”。 
2. 参数(右值)不可以是常量,因为我们需要修改右值。 
3. 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。

现在我们定义转移赋值操作符。
 

 MyString& operator=(MyString&& str) { 
    std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
    if (this != &str) { 
      _len = str._len; 
      _data = str._data; 
      str._len = 0; 
      str._data = NULL; 
    } 
    return *this; 
 }

这里需要注意的问题和转移构造函数是一样的。 
增加了转移构造函数和转移复制操作符后,我们的程序运行结果为 :

由此看出,编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符。节省了资源,提高了程序运行的效率。 
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。

关于std::move()和std::forward  再次推荐一本书:《effective modern C++》 
英文版的,这里有篇关于其中item25的翻译不错

请看这里

但是这几点总结的不错


std::move执行一个无条件的转化到右值。它本身并不移动任何东西;
std::forward把其参数转换为右值,仅仅在那个参数被绑定到一个右值时;
std::move和std::forward在运行时(runtime)都不做任何事。
 

 STD::

在C++11中,标准库在<utility>中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

std::move


函数功能

std::move(t) 负责将表达式 t 转换为右值,使用这一转换意味着你不再关心 t 的内容,它可以通过被移动(窃取)来解决移动语意问题。

 1.C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
 2.std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
 3.对指针类型的标准库对象并不需要这么做.
原型:


template <typename T>

typename remove_reference<T>::type&& move(T&& t)

{

	return static_cast<typename remove_reference<T>::type&&>(t);

 用法:

1.Movable classes:

C++11 defines support for moving. This is a performance optimization for avoiding unnecessary copying of data.

If you define a move constructor, define the corresponding move assignment operator, and vice-versa. Define move operations for classes that often are returned by value.

When defining move operations, the original object must be left in a delete-able state.

class Foo 
{
public:
  Foo(Foo&& other) : m_attribute(std::move(other. m_attribute)) {}
  Foo& operator=(Foo&& rhs) { m_attribute = std::move(rhs.m_attribute); }
private:
  std::vector<int> m_attribute;
};

 Classes which do not deal with dynamically allocated resources explicitly should (in most cases) use the default move operations.

Foo(Foo&& other) : m_attribute(std::move(other. m_attribute)) = default;
Foo& operator=(Foo&& rhs) = default;

 

总结:

std::move实现,首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast<T>之所以能使用类型转换,是通过remove_refrence<T>::type模板移除T&&,T&的引用,获取具体类型T。
 

 

std::forward

完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。C++11 lets us perform perfect forwarding, which means that we can forward the parameters passed to a function template to another function call inside it without losing their own qualifiers (const-ref, ref, value, rvalue, etc.).
函数功能

std::forward<T>(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。

std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值

class Foo
{
public:
    std::string member;
    template<typename T>
    Foo(T&& member): member{std::forward<T>(member)} {}
};

 

传递一个lvalue或者传递一个const lvaue

  • 传递一个lvalue,模板推导之后 T = std::string&
  • 传递一个const lvaue, 模板推导之后T = const std::string&
  • T& &&将折叠为T&,即std::string& && 折叠为 std::string&
  • 最终函数为: Foo(string& member): member{std::forward<string&>(member)} {}
  • std::forward<string&>(member)将返回一个左值,最终调用拷贝构造函数

传递一个rvalue

  • 传递一个rvalue,模板推导之后 T = std::string
  • 最终函数为: Foo(string&& member): member{std::forward<string>(member)} {}
  • std::forward<string>(member) 将返回一个右值,最终调用移动构造函数;

std::move和std::forward本质都是转换。std::move执行到右值的无条件转换。std::forward只有在它的参数绑定到一个右值上的时候,才转换它的参数到一个右值。

std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一byte都没有。

 

std::move()和std::forward()对比

  • std::move执行到右值的无条件转换。就其本身而言,它没有move任何东西。
  • std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。
  • std::move和std::forward只不过就是执行类型转换的两个函数;std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一byte都没有。
  • std::forward<T>()不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变;

实例:


#include <iostream>
#include <type_traits>
#include <typeinfo>
#include <memory>
using namespace std;

struct A
{
    A(int&& n)
    {
        cout << "rvalue overload, n=" << n << endl;
    }

    A(int& n)
    {

        cout << "lvalue overload, n=" << n << endl;
    }
};

 
class B
{
public:

    template<class T1, class T2, class T3>
    B(T1 && t1, T2 && t2, T3 && t3) :
        a1_(std::forward<T1>(t1)),
        a2_(std::forward<T2>(t2)),
        a3_(std::forward<T3>(t3))
    {
    }
private:
    A a1_, a2_, a3_;
};

 
template <class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
    //return std::unique_ptr<T>(new T(std::forward<U>(u)));
    return std::unique_ptr<T>(new T(std::move(u)));
}

 
template <class T, class... U>
std::unique_ptr<T> make_unique(U&&... u)
{
    //return std::unique_ptr<T>(new T(std::forward<U>(u)...));
    return std::unique_ptr<T>(new T(std::move(u)...));
}
int main()
{
    auto p1 = make_unique1<A>(2);

    int i = 10;
    auto p2 = make_unique1<A>(i);

    int j = 100;
    auto p3 = make_unique<B>(i, 2, j);
    return 0;
}

std网页实例:

// forward example
#include <utility>      // std::forward
#include <iostream>     // std::cout

// function with lvalue and rvalue reference overloads:
void overloaded (const int& x) {std::cout << "[lvalue]";}
void overloaded (int&& x) {std::cout << "[rvalue]";}

// function template taking rvalue reference to deduced type:
template <class T> void fn (T&& x) {
  overloaded (x);                   // always an lvalue,*because here x has name,evan type is rvale,but he is lvalue
  overloaded (std::forward<T>(x));  // rvalue if argument is rvalue
  overloaded(0);
}

int main () {
  int a;

  std::cout << "calling fn with lvalue: ";
  fn (a);
  std::cout << '\n';

  std::cout << "calling fn with rvalue: ";
  fn (0);
  std::cout << '\n';

  return 0;
}

 

 

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值