C++

C++ T&&推导T的类型

Posted on 2021-02-27,12 min read

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

  1. 当你想要拷贝range的元素时,使用for(auto x : range).

  2. 当你想要修改range的元素时,使用for(auto && x : range).

  3. 当你想要只读range的元素时,使用for(const auto & x : range).

  4. 其他的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 的原理在 于巧妙的利用了模板类型推导中产生的差异。


下一篇: MIT Missing Semester in CS Education : Shell Notes→