在 C++11
中加入了一个新的关键字 decltype
,用于检查实例和表达式的类型。我们来看它的用法:
decltype(entity)
实例decltype(exprssion)
表达式
这将产生实例或者表达式的类型 T
。当然这里也分为几种情况:
- 实参为亡值(xvalue),那么将产生右值引用
T&&
; - 实参为左值(lvalue),那么将产生左值引用
T&
; - 实参为纯右值(rvalue),那么将产生原类型
T
。
当然,在这里 C++
也做出了一些规定:对于一个标识符 x
来说,decltype(x)
与 decltype((x))
并不同。
1 2 3 4 5 |
struct A { double x; }; const A* a; decltype(a->x) y; // y 的类型是 double(其声明类型) decltype((a->x)) z = y; // z 的类型是 const double&(左值表达式) |
亡值通常是通过移动语义(std::move
)产生的。与 decltype
相同,std::move
也是 C++11
中引入的新特性。
简单讲述一下移动的用处。我们可以考虑一个场景,我们定义了一个 class X
,当我们需要初始化 X
时,我们可能定义了一个函数用于返回一个构造好的 X
。但是这时问题就出现了,在 C++11
之前,我们只有拷贝构造函数和默认构造函数,这时在函数中构造的临时变量 X'
将被释放,然后重新产生一个新的实例 X
。实际上,这里有一次多余的构析和构造,这是我们不希望看到的。我们希望这个临时的 X'
被直接移动给 X
。而在 C++11
中,我们可以使用 std::move
将一个右值转换为右值引用(也就是亡值引用),这样就可以直接调用 X
的移动构造函数。
一般来说:
- 移动的性能优于复制;
- 移动过后的变量不保证可用性;
- 使用移动之后的变量有可能产生 UB。
但是需要注意的是,实际上并没有一条规则保证这几点。这取决于移动构造函数的实现。假设移动构造函数的实现与拷贝构造函数相同,那么移动构造就与拷贝没有区别。
左值的概念我们相对熟悉。可以取地址的变量都是左值,当然这里指的是内存中的地址。对于表达式中产生的一些值,它们会被存入 CPU 的寄存器中而非内存中。所以:
1 2 3 |
int val; val = 1; // val is lvalue // (val + 1) = 2; error (val + 1) 无法取地址 |
当然像一些返回左值引用的函数或者表达式自然也可以作为左值,例如 std::vector
的 []
运算符。不过并不是所有左值都可以被赋值,也就是被放在等号左边。例外是 const
修饰的类型,它无法修改。对其的修改是 UB。
对于一个左值来说,它可以转化为右值。这就是为什么变量可以出现在等号右边。
在 C
中只有左值有 CV
限定,也就就是 const, volatile
限定。而在 C++
中,左值和类右值均有 CV
限定(内置类型的右值没有 CV
限定,比如 int
)。所以内置类型的左值转换为右值之后,会失去 CV
限定,其类型就是 T
本身。当然这里不展开讨论关于 CV
的内容。
对于一个表达式,如果其不是左值,那么它就是右值。不过右值不能隐式转化为左值,但可以使用 *
解引用来让右值变为左值。例如:
1 2 3 4 |
int a[5] = {}; int *p = &a[0]; *(p + 1) = 1; // (p + 1) 是一个右值 // 合法 *(p + 1) 是一个左值 |
当然,我们可以使用 &
取地址让一个左值变成一个右值。
在理解以上内容之后,我们来看一些 decltype
关键字的示例。在以下的例子中,我将解释“为什么数组名不是指针”。
实际上这张图已经非常明显了,CLion
已经告诉我们其类型了。数组名就指代了数组本身,其类型也是数组(也就是 int[5]
),而绝不是什么指针。实际上,数组名指代指针这种说法,完全是瞎扯。数组名不指代数组,去指代一个和它没有直接关系的类型,这算是什么逻辑?接下来我们看到第五行的代码,实际上这是由于编译器做了隐式转换,它会将数组名隐式转换为指针。我们加上 +
这一运算符,就完成了这个隐式转换的过程。
与之相似,函数名同样指代函数,而绝不是什么函数指针。
当然 decltype
常常在 lambda
表达式中使用。我们可以比较其与 auto
的区别。
auto
类似于模板匹配,其只会匹配 typename
而不会匹配引用(这里包括左值引用和右值引用)。如果需要声明引用则需要显示的写出 &
符号。而 decltype
则会保留引用。
所以在泛型编程中我们可以使用 auto -> decltype()
的方式简化我们的代码。
使用 auto
声明的变量必须初始化,而使用 decltype
则未必(只有产生引用类型必须初始化)。
总结:
decltype
关键字类似于auto
关键字- 都可以产生对应的类型
- 都可以作为类型说明符和占位类型说明符。
- 对于
CV
限定符的处理不同 - 对于引用的处理不同
- 在
C++17
中auto
可用作声明结构化绑定 - 在
C++20
中auto
可作为模板参数
参考资料:
- https://learn.microsoft.com/zh-cn/cpp/cpp/decltype-cpp?view=msvc-170
- https://www.bilibili.com/video/BV1mw41177uK/
- https://zh.cppreference.com/w/cpp/keyword/auto
- https://learn.microsoft.com/zh-cn/cpp/c-language/l-value-and-r-value-expressions?view=msvc-170
- https://nettee.github.io/posts/2018/Understanding-lvalues-and-rvalues-in-C-and-C/
- https://zh.cppreference.com/w/cpp/language/decltype
- https://www.bilibili.com/video/BV1jY4y1o7Dg/
以上,感谢阅读。
发表回复