20200307
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
参考: