C++中的值类别与模板推导

C++ 中表达式的值类别

C++ 中的表达式有两个属性,分别是值类型(type)和值类别(value category),每个表达式都有着某些非引用类型,且每个表达式都属于三种主要的值类别之一,这三种值类别分别是:左值(lvalue),纯右值(prvalue),将亡值(xvalue)。

  1. 泛左值(glvalue,generalized lvalue):这种表达式决定一个对象或者函数的标识,比如变量名,函数名。
  2. 纯右值(prvalue,pure rvalue):
    1. 计算某个运算对象的内置运算符的值,这种 prvalue 没有结果对象。
    2. 初始化一个结果对象。
  3. 将亡值(xvalue,expiring value):将亡值是对象的资源被标记为可以被重复使用的泛左值。
  4. 左值(lvalue):左值指的是不是将亡值的泛左值。
  5. 右值(rvalue):纯右值和将亡值统称为右值。

左值

  • 变量名,函数名或者数据成员,无视类型。无论变量是左值引用或者右值引用,由变量名称组成的表达式始终是一个左值表达式。
  • 一个函数调用或者运算符重载表达式,它们的返回类型为左值引用,例如 std::cout << 1
  • 内置的赋值和复合赋值运算符,例如 a = 1b += 1
  • 内置的前置递增或者递减运算符,++a--a
  • 字符串字面量。
  • 一个类型转换表达式,目标类型为左值引用。
  • 一个函数调用或者运算符重载表达式,它的返回类型为函数的右值引用。
  • 一个类型转换表达式,它的目标类型为函数的右值引用。

纯右值

  • 字面量,例如 42truenullptr。注意字符串字面量不是纯右值。
  • 一个函数调用或者运算符重载表达式,它们的返回类型为非引用类型,例如 str.substring(1,2)
  • a++a--,内置的后缀递增或者递减运算符。
  • a+ba-ba & ba << b,其他内置的算术运算符。
  • a && ba || b!a,内置的逻辑运算符。
  • a < ba == ba > b,内置的比较运算符。
  • &a,内置的取址运算符。
  • 一个类型转换表达式,目标类型为非引用类型,例如 static_cast<double>(x)
  • this 指针。
  • 枚举器。
  • 标量类型的非类型模板参数。
  • 一个 lamada expression

将亡值

  • 一个函数调用或者函数重载表达式,它的返回类型是一个右值引用,例如 std::move(x)从C++11开始)。
  • 一个类型转换表达式,它的目标类型为右值引用,例如 static_cast<char&&>(r)从C++11开始)。
  • 任何一个表明了临时对象的表达式,参考 temporary materialization(从C++17开始)。

temporary materialization 请参考这篇文章

将亡值表达式也会标识一个对象,毕竟它也属于泛左值这个大类里面,与左值不同的是,将亡值会将对象标记为资源可复用的对象,这在移动语义里面起着至关重要的作用。

模板函数类型推导

在理解了值类别之后,就可以开始讨论模板函数的类型推导了。整个模板的类型推导十分复杂,这里不会覆盖整个模板推导,仅仅包含模板函数的推导,而且只与引用有关。

左值引用

考虑如下模板函数

template<typename T>
void foo(T& t)
{
    
}

这个模板函数的参数是左值引用,所以这个模板函数希望能够接收一个左值表达式。需要注意的是,即使你传递的是右值引用,它仍然还是一个左值表达式,如下代码所示:

int&& a = 1;
foo(a);

但是这种形式下,传右值是不可以的,因为如果要将左值引用绑定到一个右值,它必须是一个常量左值引用,因此,如下代码是正确的:

class Foo
{

};

template<typename T>
void Foo(const T& a);

foo(Foo());//纯右值
Foo f;
foo(std::move(f));//将亡值

引用转发

引用转发是形如以下的模板函数声明:

template<typename T>
void foo(T&& t)
{
    
}

我们用 T&& 形式来标识一个引用转发。引用转发的作用是,当你传递一个左值表达式时,模板类型参数会被推导为一个左值引用,例如如下代码:

int a = 1;
foo(a);//因为a是一个左值表达式,所以T被推导为int&

如果传递的是一个右值,那么模板类型参数会被推导为右值表达式的值类型,例如如下代码:

foo(1);//纯右值,T被推导为int
foo(std::move(1));//将亡值,T被推导为int

右值引用

右值引用非常类似于引用转发,它的形式如下:

template<typename T>
void foo(const T&& t)
{

}

右值引用比引用转发多了一个 const,这个模板希望接收一个右值表达式参数,因此以下的代码是不正确的:

int a =1;
foo(a);//我们传递了一个左值表达式,但是foo模板函数只能接收右值表达式

但是以下的代码是合法的:

foo(1);

int&& a=1;
foo(std::move(a));

int b = 1;
int& c = b;
foo(std::move(c));

参考

posted @ 2023-02-07 18:40  atorli  阅读(202)  评论(0)    收藏  举报