[译] Item 3: Understand Decltype
decltype
是一个奇怪的发明。给一个名字或者表达式,decltype
可以告诉你这个名字或者表达式的类型。通常的情况下,他告诉你的都是你预期的。但是偶尔也会有些出人意料。
我们从最典型的情况开始 —— 那些不会让你惊奇的情况。与模板类型推导和 auto
类型推导不同,decltype
通常只是鹦鹉学舌般的返回你传入的名字或表达式的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
是不是完全没有惊喜?
在 C++11 中,decltype
的作用也许就是声明一个返回值类型依赖于模板参数的模板函数。举个例子,假设我们准备写一个以容器 (接受下标操作的) 和索引 index
为参数的函数,验证用户之后返回。返回值的类型与容器下标操作的类型相同。
T
类型容器的 operator[]
返回值类型通常是 T&
。对于 std::deque
来说,是这个样,对于 std::vector
来说,大部分情况下也是的,不过对于 std::vector<bool>
来说,情况就不一样了,它的返回值不是 bool&
。这就是我们要在 Item 6 中讨论的问题。在这里,最重要的是,我们要知道容器的 operator[]
的返回值依赖于容器。
decltype
可以让这个问题变得简单,下面的代码展示如何利用 decltype
来计算返回值。它还可以进一步的精简,我们稍后再说:
1 2 3 4 5 6 |
|
函数名前面的 auto
在类型推导中什么用处都没有。取而代之的是 C++11 的尾随返回值类型 (trailing return type)。函数返回值的类型在函数参数列表之后声明 (在 –> 符号之后)。尾随返回值类型的好处是,函数的参数可以用于声明返回值类型。在 authAndAccess
中,我们的返回值类型声明用到了参数 c
与 i
。如果我们将返回值类型置于函数名前,那么我们就无法使用 c
和 i
,因为这时它们还没有声明。
上面的声明中,autoAndAccess
的返回值与我们预期的完全一致,就是容器 Container
的 operator[]
的返回值类型。
C++11 支持单句 lambda
的返回值类型推导,而 C++14 进行了扩展,支持所有 lambda
和函数,包括那些多条语句的。这意味着在上面的例子中,使用 C++14 我们可以省略掉尾随返回值类型,仅仅保留最前面的 auto
即可。在这里,auto
是类型推导的占位符。编译器会根据函数的具体实现来推导函数的返回值类型。
1 2 3 4 5 6 |
|
但是,在这里返回值类型推导使用的是哪个类型推导规则?模板? auto
? 还是 decltype
?
也许你会有些惊讶,函数的 auto
返回值类型遵循的是模板类型推导规则。看起来 auto
类型推导规则,在这里是一个更好的选择,不过 auto
类型推导与模板类型推导机会是完全一致的。唯一的区别就是模板类型推导无法推导大括号初始化。
在这里,authAndAccess
的返回值类型推导使用模板类型推导是有问题的,不过 auto
类型推导也是一样存在问题。这里的问题是,我们需要推导的表达式是一个引用。
回想一下之前的讨论,operator[]
对于大多数容器类型 T
来说,返回值类型都是 T&
,我们在 Item 1 中已经讨论过了,在模板类型推导时,表达式的引用会被忽略。考虑下这对我们上面的代码意味着什么:
1 2 3 |
|
上面的代码中,d[5]
返回类型是 int&
,但是 auto
返回值类型推导会将引用忽略掉,变成了 int
类型。而 int
类型作为一个函数的返回值,是一个右值,而上面的代码中企图将 10
赋值给一个右值。这在 C++ 中是禁止的,因此会编译失败。
这个问题是由于我们使用了会忽略引用的模板类型推导。在这里,我们需要的实际上是 decltype
类型推导。它能够保证返回值类型与 c[i]
的类型完全一致。
C++ 将引入新的类型推导规则,decltype
类型推导,在 C++14 中通过标识符 decltype(auto)
来实现。这看起来有些奇怪,但是很好的表达了意图:auto
是要推导的类型,decltype
表明需要遵循 decltype
类型推导规则。现在我们可以把之前的代码改成下面这样了:
1 2 3 4 5 6 |
|
现在 authAndAccess
的返回值与 c[i]
的类型完全一致了,一般情况下当 c[i]
返回 T&
类型时,authAndAccess
也会返回 T&
,而当 c[i]
需要返回一个 object 类型时,authAndAccess
也会返回 object 类型。
decltype(auto)
的使用,不仅限于函数返回值类型,当你需要使用 decltype
类型推导规则时,它可以用于声明变量:
1 2 3 4 5 6 7 |
|
但肯定还有两个问题困扰着你,一个就是上面的代码中提到的优化,我们到现在还没有谈到,现在就让我们来看这个问题。
回头看看我们的 C++14 版本的 authAndAccess
函数声明;
1 2 |
|
容器参数的类型是左值引用,这样可以让容器返回元素供调用者修改。但是这意味着这个函数无法接受右值的容器作为参数了,因为右值是无法绑定到左值引用的。
不可否认,传一个右值给 authAndAccess
的场景非常少见。一个右值的容器作为一个临时对象会在函数 authAndAccess
结束时销毁,这意味着容器元素的引用( authAndAccess
函数的返回值)会失效。但是给 authAndAccess
传入一个临时对象还是有意义的。调用者有时会需要一个容器元素的拷贝的,比如下面的代码:
1 2 3 4 5 |
|
支持上面的用法,意味着我们要将原本的函数修改为同时支持左值和右值。重载是可以解决这个问题,但是这样一来我们就需要维护两个函数了。有一种办法可以避免同时维护两个函数,我们可以让函数 authAndAccess
同时支持左值以及右值参数,Item 24 中,我们会详细的介绍全局引用。修改后的 authAndAccess
函数声明如下和:
1 2 |
|
在上面的模板中,我们不知道 Container
的类型,同时我们也忽略了 index 对象的类型。对一个未知类型使用值传递会因为不必要的拷贝而造成性能问题,也会有对象切割问题(Item 41),还会被同事吐槽,不过在这里我们只考虑标准库容器的情况(比如,std::string
, std::vector
以及 std::deque
的 operator[]
),在这里仍然坚持使用值传递。
不过我们还需要更新一下模板函数的实现,根据 Item 25,我们使用 std::forward
把全局引用包起来,
1 2 3 4 5 6 |
|
上面的代码完全符合我们的需求,不过需要支持 C++14 的编译器。如果你现在还没有支持 C++14 的编译器的话,那就需要一个 C++11 的版本。和 C++14 的版本非常相似,唯一的不同点是我们需要手动的指定返回值类型,
1 2 3 4 5 6 |
|
另一个问题 —— 除非你是一个库的作者,不然的话,这种情况基本不可能遇到。
要完全明白 decltype
的行为,你必须了解少数的特殊情况。大多数不值得在本书中讨论,不过我们现在来看一看它们是如何使用的。
对一个变量名使用 decltype
,会得到与变量名一致的类型。变量名是一个左值,但是不会影响 decltype
的行为。但对于左值表达式来说,情况就变的复杂了,它会使 decltype
返回左值引用。也就是说,如果一个左值表达式不仅仅是一个变量名,那么对于类型 T
的左值表达式使用 decltype
, 它会得到一个 T&
类型。这很少会产生冲突,因为大部分的左值表达式都内含左值引用的限定符。例如返回左值的函数通常返回的都是左值引用。
但是这还是会产生一些不期望的问题,例如,
1
|
|
x
是变量名,decltype(x)
的类型是 int
。但是,使用括号将 x
包起来,情况就不一样了。x
是一个左值, (x)
是一个左值表达式,decltype((x))
的类型是 int&
。一个括号改变了 decltype
的类型。
在 C++11 中,这不是大问题,但在 C++14 中,由于支持了 decltype(auto)
,这个微不足道的变化会变的影响含漱的返回值类型推导。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
注意,f2
不仅仅只是与 f1
的返回值类型不同,它还返回了一个局部变量的引用。这使得你的代码不知不觉的就产生了未定义行为。
这就是说当你使用 decltype(auto)
时,必须要非常小心。一些非常小的细节就可能会影响 decltype(auto)
的类型推导结果。确定类型推导的结果是否符合你的预期,你需要用到 Item 4 中介绍的技术手段。
与此同时,不要忘记从更高的角度来审视这个问题。decltype
(不论是否与 auto
在一起)偶尔会产生让你惊讶的结果,但是它不是一般情况。decltype
通常都会符合你的预期。这对于变量名来说是非常正确的。这种情况下,decltype
就像它们看起来的那样:它推导的结果就是变量声明的类型。
需要记住的
decltype
几乎总是与表达式的类型完全一致。- 对于类型为
T
的左值表达式(除了变量名),decltype
的类型为T&
。 - C++14 支持
decltype(auto)
,与auto
类似,在初始化的时候推导变量的类型,但是使用decltype
类型推导规则。