右值,移动语义,完美转发

先来几个快速问答:
右值什么时候出现的?右值早就有了,右值引用直到c++11才有的
右值有什么用?通过右值引用,减少拷贝。
为什么右值引用能减少拷贝?右值赋值给右值引用变量的时候,直接将右值对应的地址交给右值引用变量,相当于对应的内存换了个马甲。
什么时候用右值引用?一般都是将左值转换成右值,然后赋值给右值引用,来减少拷贝。

完美转发是个什么玩意?右值引用的赋值只能用一次,函数嵌套需要多次就需要完美转发了。

来几个例子

int a = 10;
int b = 20;
int *pFlag = &a;
vector<int> vctTemp;
vctTemp.push_back(1);
string str1 = "hello";
string str2 = "world";
const int &m = 1;

请问,a,b, a+b, a++, ++a, pFlag, *pFlag, vctTemp[0], 100, "hello", str1, str1+str2, m分别是左值还是右值?

a和b都是持久对象(可以对其取地址),是左值;
a+b是临时对象(不可以对其取地址),是右值;
a++是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而那份拷贝是临时对象(不可以对其取地址),故其是右值;
++a则是使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左值;
pFlag和*pFlag都是持久对象(可以对其取地址),是左值;
vctTemp[0]调用了重载的[]操作符,而[]操作符返回的是一个int &,为持久对象(可以对其取地址),是左值;
100是数值字面量,不可以取地址,是右值;
"hello"是字符串字面量,但在C++中是可以取地址的,所以是左值
str1是持久对象(可以对其取地址),是左值;
str1+str2是调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右值;
m是一个常量引用,引用到一个右值,但引用本身是一个持久对象(可以对其取地址),为左值。

上面例子的右值都是纯右值,纯右值还包括:非字符串字面量,临时对象,函数返回的非引用对象。
还有一类右值,称为将亡值,包括:函数返回右值引用对象,通过static_cast<T&&>()和std::move<T>()强制转换后的对象


非常量左值引用只能获取非常量左值,常量左值引用可以获取任意的值。
非常量右值引用只能获取非常量右值,常量右值引用只能获取常量右值或非常量右值
为什么?
非常量肯定只能获取非常量。不然获取常量了,那个常量就可以变了?不合理。
常量可以获取非常量,然后这个非常量就不能变了。
非常量左值只能获取左值,因为右值在没有被赋值前都是常量


但是有一点要注意,看到&&不一定都是右值引用,还有可能是万能引用
例如:

template<typename T>
T add(T&& a, T&& b)
{
    return a+b;
}

这里并不知道T具体是什么类型,只有运行的时候才知道

这里的T&&既可以是右值引用,也可以是左值引用,叫万能引用
到底是什么要看你传进去个什么

如果 T 传进 int左值, 那 T 就是 int&
如果 T 传进 int右值,那 T 就是 int
如果 T 传进 int左值引用,那 T 就是 int&
如果 T 传进 int右值引用,那 T 就是 int&&

然后 T&& 再发生引用折叠
int& && --> int&
int && --> int&&
int&& && --> int&&

万能引用就是这么怪

注意,如果T不是推到型,就不存在上面的规则
例如下面的情况不是万能引用

template<typename T>
T add(std::vector<T>&& vec)
{
...
}

因为vec传进来已经确定类型了,它就代表std::vector<T>的右值引用

还有:

int&& r = 10int x = add(r,20);

这里的r虽然是右值引用,但是,r本身是左值! T&& 就是 int&

除非:

int x = add(std::move(r),20);

T&& 才是 int&&


做个实验

class shape {
public:
    shape() {}
    virtual ~shape() {}
};

void foo(const shape&)
{
    puts("foo(const shape&)");
}
void foo(shape&&)
{
    puts("foo(shape&&)");
}
void bar(const shape& s)
{
    puts("bar(const shape&)");
    foo(s);
}
void bar(shape&& s)
{
    puts("bar(shape&&)");
    foo(s);
}

int main()
{
    bar(shape());
}

输出结果是:

bar(shape&&)
foo(const shape&)

shape()生成一个临时对象,是纯右值,优先调用void bar(shape&& s)
内部foo(s)时,s是左值,优先调用void foo(const shape&)


所以在函数嵌套调用的时候,参数的类型就变了,本来是右值引用的,就成左值引用了,有什么办法能不变吗?
有,参数用完美转发(forward)包一下,forward代码如下

template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{
    return static_cast<_Tp&&>(__t);
}

template<class _Ty>
inline constexpr _Ty&& forward( typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
{
  static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
  return (static_cast<_Ty&&>(_Arg));
}

当传入是左值时, _Tp推到为左值引用,强制转化时折叠成左值引用,返回还是左值引用
当传入是右值时, _Tp推到为右值, 强制转化时折叠成右值引用,返回还是右值引用
当传入是左值引用时,_Tp推到为左值引用,强制转化时折叠成左值引用,返回还是左值引用
当传入是右值引用时,_Tp推到为右值引用,强制转化时折叠成右值引用,返回还是右值引用


再看下move的代码

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept
{
    return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}

_Tp推到成什么不重要,重要的是,强制转化为右值引用,然后返回还是折叠成右值引用。


总结下:
forward()的作用是传进去是什么手性(左右), 返回还是什么手性
move() 的作用是不管传进去什么手性(左右),返回都是右值引用


下面是一个万能函数包装器,任何参数都可以"完美转发"
注意下面的...不是省略,是可变参数语法

template<class Function, class... Args>
inline auto FuncWrapper(Function && f, Args && ... args) -> decltype(f(std::forward<Args>(args)...))
{
    //typedef decltype(f(std::forward<Args>(args)...)) ReturnType;
    return f(std::forward<Args>(args)...);
    //your code; you can use the above typedef.
}

posted on 2020-02-12 00:36  litandy  阅读(173)  评论(0编辑  收藏  举报

导航