在静态类型语言中,一个变量需要由类型说明符指定,而随着 C++ 的发展,类型也可以从表达式推导出来,不必显式写出。
一切始于 C++11
decltype(E)
,Decltype 也属于说明符,接受一个表达式参数。也可以传入一个变量,因为变量名属于
id-expressions
,也是表达式。
这里的核心在于,表达式其实包含三部分信息:type, value, 和 value category。使用 Decltype 推导出的表达式结果与原类型的信息并不总是相同,这种不一样的依据就是类型推导规则。
Decltype 的推导规则需要分为两种情况讨论,
E
和
(E)
,也就是说,多加一个括号将改变推导规则。
先来看第一种,
E
的情况。
如果
E
是 id-expressions 或者类成员名称访问,此时推导结果的 type 和 value 都和
E
所对应的实体相同,但是不会保留原有的 value category。
1int a = 42;
2static_assert(std::is_same_v<decltype(a), int>);
3std::println("lvalue: {}", std::is_same_v<decltype(a), int&>); // false
4std::println("prvalue: {}", std::is_same_v<decltype(a), int>); // true
5std::println("xvalue: {}", std::is_same_v<decltype(a), int&&>); // false
a
是一个 lvalue,而
decltype(a)
是一个 prvalue。
再来看第二种,
(E)
的情况。
如果
(E)
是 id-expressions 或者类成员名称访问,推导时 value 不变。对于 type,若
E
是 lvalue,推导的 type 为
T&
;若
E
是 xvalue,type 为
T&&
。同时也会保留 value category。
1int a = 42;
2static_assert(std::is_same_v<decltype((a)), int&>);
3std::println("lvalue: {}", std::is_same_v<decltype((a)), int&>); // true
4std::println("prvalue: {}", std::is_same_v<decltype((a)), int>); // false
5std::println("xvalue: {}", std::is_same_v<decltype((a)), int&&>); // false
a
是 lvalue,推导类型为
T&
,依旧是一个 lvalue。
核心就记住这两条规则即可,需要注意
(E)
推导的不只是实体的类型,还附加有实体所在的环境,就是规则中的
T&
所指,比如:
1struct
A { double x; };
2const A* a;
3
4decltype(a->x) y; // double
5decltype((a->x)) z = y; // const double&
再比如:
1void f() {
2 float x, &r = x;
3 [=] {
4 decltype(x) y1; // float
5 decltype((x)) y2 = y1; // const float&
6
7 decltype(r) r1 = y1; // float&
8 decltype((r)) r2 = y2; // const float&
9 }
10}
由于 Lambda expressions 默认是不可修改的,因此使用
(x)
推导时会带上
const
。
到此为此,本文第一部分结束,接着让我们更进一步,看 Placeholder Type 的推导。
Placeholder Type
C++ 存在两种类型的 Placeholder Type 说明符,
auto
和
decltype(auto)
。使用这种类型的说明符,类型名称不必再显式指定,也不必使用
decltype()
根据表达式推导,一切推导都自动完成。它们也构成了 Modern C++ 的 Left-to-Right 声明风格。
刚开始,
auto
仅是作为 Right-to-Left 风格的代替语法,以下两种声明意义完全相同。
1int f() {}
2auto f() -> int {}
这里只是换了一种语法形式,并不存在类型推导,返回类型由 trailing-return-type 显式指出。
若不显式从 trailing-return-type 指定,此时将推导类型。例如:
1auto f() {}
2decltype(auto) g() {}
它们两个的首要不同来源于语法,
auto
可以和其他修饰符组合出现,如
const auto&
,而
decltype(auto)
必须单独出现,不能添加任何修饰符。
推导规则是另外一个不同点,
auto
使用的是 TAD 规则,而
decltype(auto)
使用的是本文第一部分介绍的
decltype(E)
推导规则。
下面看具体的几条规则。
第一条规则,
auto
推导时总是以 value 返回,不会返回引用,而
decltype(auto)
的规则支持动态返回。
1auto f(int& a) {
2 return a;
3}
4
5decltype(auto) g(int& a) {
6 return a;
7
}
8
9int x = 42;
10static_assert(std::is_same_v<decltype(f(x)), int>);
11static_assert(std::is_same_v<decltype(g(x)), int&>);
示例中
f()
永远返回
int
,而
g()
可以返回
int&
。但是
auto
可以和修饰符组合使用,因此你也可以这样来返回引用:
1auto& f(int& a) {
2 return a;
3}
4
5int x = 42;
6static_assert(std::is_same_v<decltype(f(x)), int&>);
再看回
decltype(auto)
,推导起来其实相当于
decltype(a)
,类型就是实体
a
的类型。
TAD 的内容在
洞察函数重载决议
中已经详细讨论过,在此不再细述。需要注意,
auto
使用 TAD 的规则推导,所以推导出来的类型也并不一定与原实体类型一致。例子:
1const int b = 0;
2auto c = b; // c is an int
3static_assert(std::is_same_v<decltype(c), int>);
这与
decltype(auto)
的行为完全不一致:
1const int b = 0;
2decltype(auto) c = b; // c is an int const
3static_assert(std::is_same_v<decltype(c), int const>);
只要谨记这条规则,就知道何时该使用哪种 Placeholder Type 了。
第二条规则,重定义函数,或是特化函数模板时,如果本身就使用的是 Placeholder Type,那么也应该使用相同的形式。
1auto f(); // OK
2auto f() { return 42; } // OK
3auto f(); // OK
4int f(); // error
5decltype(auto) f(); // error
6
7decltype(auto) g(); // OK
8decltype(auto) g() { return 42; } // OK
9decltype(auto) g(); // OK
10int g(); // error
11auto g(); // error
下面是一个函数模板的例子:
1template <class T> auto f(T t) { return t; } // #1
2template char f(char); // error, no matching template
3template auto f(int); // OK, return type is int
4template<> auto f(double); // OK, forward declration with unknown return type
5
6template <class T> T f(T t) { return t; } // OK, not functionally equivalent to #1
7template auto f(float); // OK, still matches #1
8template char f(char); // OK, now there is a matching template