20200307

@ShiKaiWi

Plan

180min: CPP expression's type & value category

Notes

expression's type & value category

在看模板的时候意外的遇到一个棘手的问题,可以看下面一个例子:

#include <iostream>

void f2(int &&x) {}

void f1(int &&x) { f2(x); }

int main() {
  int x = 10;
  f1(std::move(x));
}

这里乍看之下并无不妥,在 f1 中进行了 f2 的调用,并且 x 的类型和 f2 的形参类型一致,但是编译器却会是报错的:

expr.cc:5:20: error: no matching function for call to 'f2'
void f1(int &&x) { f2(x); }
                   ^~
expr.cc:3:6: note: candidate function not viable: no known conversion from 'int' to 'int &&' for 1st argument
void f2(int &&x) {}
     ^
1 error generated.

报错很简单,就是 f2 的调用类型不匹配,不存在 int 到 int&& 的转换。

这里第一个非常奇怪的点就是 x 的类型不是 int&& 吗?怎么突然变成了 int?整个过程非常曲折,阅读了很多文档,才知道这样一个规则:

Each expression has some non-reference type

也就是说,调用 f2(x) 的时候,x 不再是一个 variable 了,已经成为了一个 expression,而 expression 是没有引用类型的,他就会直接转换成 int 类型,这个是符合逻辑的,因为引用类型本来就是一个别名,真正用这个别名作为表达式的时候,它必然是原始的值类型。

这个报错信息是看懂了,也搞明白了 x 的类型是 int,但是为什么会报错呢?要知道就算是一个 int 实参,也是可以绑定到一个 int& 或者 int&& 的形参上面的呀(比如 f1(5) 这样的调用)?

那么其实还是存在问题的,搜索资料之后我发现一个 expression 不仅仅有 type 这一个属性,还包含着 value category 这样的类型, 比较常见的说法是 lvalue 和 rvalue,不过实际上是存在更精细的划分的:

             ┌─────────────────┐            
             │ value category  │            
             └─────────────────┘            
                      │                     
              ┌───────┴───────┐             
       ┌──────▼───┐     ┌─────▼────┐        
       │ glvalue  │     │  rvalue  │        
       └──────────┘     └──────────┘        
             │                │             
      ┌──────┘────────┬───────┴───────┐     
┌─────▼────┐    ┌─────▼────┐    ┌─────▼────┐
│  lvalue  │    │  xvalue  │    │ prvalue  │
└──────────┘    └──────────┘    └──────────┘

也就是说 f1(5) 这个调用能够成功绑定实参和形参是因为:

  • 5 具备了 int 这个 type
  • 5 具备了 prvalue 这个 value category 因此 f1(int&& x) 形参 x 需要的是一个 reference to rvalue 就成立啦。

那么再回到我们的疑惑,f2(x) 的失败原因是什么,先说结论,现在 x 作为一个 expression 满足:

  • type 是 int
  • value category 为 lvalue

那么大家疑惑的就是第二点了,x 为什么是一个 lvalue? lvalue 和 rvalue 详细的划分可以参考: https://en.cppreference.com/w/cpp/language/value_category,这里可以说一下直觉上的理解,x 一个变量(variable),而在这里这个变量 x 自然是一个 lvalue(可以对 x 进行赋值)。

最后给上正确的代码:

#include <iostream>

void f2(int &&x) {}

void f1(int &&x) { f2(std::move(x)); }

int main() {
  int x = 10;
  f1(std::move(x));
}

另外可以通过 std::forward 和模板,也有另一种写法:

#include <iostream>

void f2(int &&x) {}

template<typename T>
void f1(T &&x) { f2(std::forward<T>(x)); }

int main() {
  int x = 10;
  f1(std::move(x));
}

模板

模板实参推断的逻辑很简单,就是根据传入的实参,推断模板函数的模版参数是什么,其中有两条转换规则:

  • 任何实参类型都可以转换为 const 类型。
  • 数组和函数的类型可以转换为指针。

More

参考: