T&&需要推导T的类型时的规则是什么?
为什么最好用 auto&&的形式?
非常量左值引用不能绑定右值
常量左值引用可以绑定右值
右值引用只能绑定右值
为什么需要完美转发?
右值语义的引出
void set(const string & var1, const string & var2){
m_var1 = var1; //copy
m_var2 = var2; //copy
}
A a1;
string var1("string1");
string var2("string2");
a1.set(var1, var2); // OK to copy
a1.set("temporary str1","temporary str2"); //也需要copy,浪费
# 引入右值语义
void set(string && var1, string && var2){
//avoid unnecessary copy!
m_var1 = std::move(var1);
m_var2 = std::move(var2);
}
A a1;
//temporary, move! no copy!
a1.set("temporary str1","temporary str2");
#但是要重载两遍,代码重复,所以引入完美转发
template<typename T1, typename T2>
void set(T1 && var1, T2 && var2){
m_var1 = std::forward<T1>(var1);
m_var2 = std::forward<T2>(var2);
}
/*
forward 能够转发 [const] T &[T] 的所有情况
const T &
T &
const T &&
T &&
when var1 is an rvalue, std::forward<T1> equals to static_cast<[const] T1 &&>(var1)
when var1 is an lvalue, std::forward<T1> equals to static_cast<[const] T1 &>(var1)
如果外面传来了rvalue临时变量, 它就转发rvalue并且启用move语义.
如果外面传来了lvalue, 它就转发lvalue并且启用复制. 然后它也还能保留const.
*/
右值引用
auto for loop
-
当你想要拷贝range的元素时,使用for(auto x : range).
-
当你想要修改range的元素时,使用for(auto && x : range).
-
当你想要只读range的元素时,使用for(const auto & x : range).
-
其他的auto变种,几乎没有作用。
T&& Doesn’t Always Mean “Rvalue Reference”
by Scott Meyers
https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
Widget&& var1 = someWidget; // here, “&&” means rvalue reference
auto&& var2 = var1; // here, “&&” does not mean rvalue reference
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference
&& is not only type declaration, you will misread a lot of c++11 code.
The essence of the issue is that “&&” in a type declaration sometimes means rvalue reference, but sometimes it means either rvalue reference or lvalue reference.
只有在类型推断(模板和auto)的时候,&&是universe reference,要看传入的值是rvalue还是lvalue:
Universal references can only occur in the form “T&&”!
例如
template <typename T>
void f(T&& param);
f(10) // rvalue
int x = 10;
f(x) // lvalue
template <typename T>
void f(const T&& param) //rvalue, not universe reference. universe reference只认 T&& 形式
//error 不能把rvalue绑定到non-const lvalue reference
// 如果把f参数改为const int& a 那就没办法对a进行改变,也有限制
#include <iostream>
using namespace std;
int f(int& a){
cout << a << endl;
}
int main()
{
cout << f(3);
return 0;
}
/*
main.cpp:9:14: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
9 | cout << f(3);
*/
//一个很好的例子,来说明右值为什么有必要存在
//given the expression E(a, b, ... , c), we want the expression f(a, b, ... , c) to be equivalent.
//不能处理f(1,2,3)
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
# 如果E中要对a b c进行修改,则void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(a, b, c);
}
#使用const_cast,但是E修改了const object,与f的函数签名不一致了
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}
#重载跨域解决这个问题 但是2^N的重载个数,太麻烦
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);
universe reference 推导规则
g1. 形如"T&&"的表达式(必须严格类似这个形式,不可有任何c-v限定符)且T是一个需要推导的东西,那么该表达式的类型可以称为universal reference。
g2. universal reference最终既可以最后推导为左值引用,也可以推导为右值引用。
==>d1. 推论1. 形如"T&&"的表达式,不一定都是右值引用。
g3. 某个变量的类型可以是右值引用,但包含该变量本身可以是左值。(只要能被取址的,就是左值)
特别的,Named variables and parameters of rvalue reference type are lvalues. (You can take their addresses.) [1]
Keep in mind, once inside the function the parameter could be passed as an lvalue to anything. [2]
G4-5 T应该如何被推导?
g4. 当形如"T&&"的universal reference被lvalue初始化时, T被推导为lvalue reference。
g5. 当形如"T&&"的universal reference被rvalue初始化时, T被推导为该rvalue的原生类型。
g6. 当形如"T&&"的universal reference所在模板函数传入的参数为引用类型(无论左值引用还是右值引用)时,实参的引用部分被忽略。
并由于传入的东西是左值(类型是引用,变量本身都为左值),T按g4规则被推导为lvalue reference。
G7-8 universal reference的整体类型最终如何确定?
g7. 当universal reference被lvalue初始化时,universal reference最终是左值引用.
g8. 当universal reference被rvalue初始化时,universal reference最终是右值引用.
g9. 重载与左值引用:
若存在一个函数的重载,其参数是左值引用或右值引用,当传入的参数为左值时,匹配左值引用的重载函数;当传入的参数为右值时,匹配右值引用的重载函数。如果有原生类型的重载函数,则语法会报错“多个重载函数匹配同一个调用”。
例子
template <typename T>
void f(T&& para) {
// do something;
}
int x;
int &&a = 10; //a的类型是int&&,右值引用
int &b = x; //b的类型是int&,左值引用
f(10); // 10是右值,para的类型是右值引用,T是10的原生类型也就是int 函数f会实例化为 f(int&& para)
f(x); // x是左值,para的类型是左值引用,T是左值引用,函数f会实例化为 f(int& && para)
f(a); // g6./g4. a是右值引用,本身是左值,T是左值引用,函数f会实例化为 f(int& && para)
f(b); // g6./g4. b是左值引用,本身是左值,T是左值引用,函数f会实例化为 f(int& && para)
a是引用,且是一个lvalue
- a是lvalue --> 所以T是int&,
- a是引用,要引用去掉,只保留本体
所以universe reference T&& param:判断T是通过传入的实参是lvalue还是rvalue;如果实参是引用就把引用符号都去掉,且一定是lvalue。
右值引用
int main() {
lambda_capture_value();
int a;
int &b = a;
// int &&c = a; //error: rvalue reference to type 'int' cannot bind to lvalue of type 'int'
}
为什么不允许non-const reference绑定到non-lvaule,因为存在逻辑错误:
void increase(int &v){
v++;
}
void foo(){
double s = 1.3;
increase(s); //报错了:int& 不能引用double类型的参数,所以必须产生一个临时值来保存s的值,从而当increase修改临时值时,s并没有被改变
}
为什么允许常量引用绑定到非左值,很简单,因为Fortan需要。
T&& + 传入的是右值,模板参数T才会被推导为右引用类型,例如T = int&&
为什么在循环语句中,auto&& 是最安全的方式:因为当auto被推导为不同的左右引用时,与&&的坍塌组合是完美转发(参考forward)
template<typename _Tp>
constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type &__t) noexcept { return static_cast<_Tp &&>(__t); }
template<typename _Tp>
constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type &&__t) noexcept {
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp &&>(__t);
}
在这份实现中,std::remove_reference 的功能是消除类型中的引用,而 std::is_lvalue_reference 用于检查类型推导是否正确,在 std::forward 的第二个实现中检查了接收到的值确实是一个左值,进 而体现了坍缩规则。
当 std::forward 接受左值时,_Tp 被推导为左值,而所以返回值为左值;而当其接受右值时,_Tp 被推导为右值引用,则基于坍缩规则,返回值便成为了 && + && 的右值。可见 std::forward 的原理在 于巧妙的利用了模板类型推导中产生的差异。