类型推导规则
在大多数情况下,模板与auto的类型推导规则一致,且规则很简单。
情况1. 没有加任何修饰
// 模板函数:
template <typename T>
f(T t) {...}
// auto声明:
auto x = ...;
这种情况下,参数是按值传递,形参t或者变量x都是一个副本,那么就需要去掉引用,且副本本身没必要加cv限定符(const、volatile,下文都不考虑volatile,只说const),也需要去掉。
const int& a = ...;
f(a); // T 推导为 int
auto x = a; // x 的类型为 int
但需要注意,只能去掉变量本身的const,对于指针类型,所指对象的const修饰不能去掉。
const int* const a = ...;
f(a) // T 推导为 const int*
auto x = a; // x 的类型为 const int*
其实不需要特别记忆,只要记住一点:在能编译通过的前提下,去掉没必要的const和&符号即可:
const int& a = ...;
// 以下语句能否编译通过?
const int& x = a; // OK
int& x = a; // 编译不通过,const int& 类型不可转为int&
const int x = a; // OK
int x = a; // OK,且去掉了多余的const和&符号
// 因此,auto x = a; 中,x类型推导为 int
const int* const b = ...;
// 以下语句能否编译通过?
int* const x = b; // 编译不通过
const int* const x = b; // OK
const int* x = b; // OK,且去除了多余的const
// 因此,auto x = b; 中,x类型推导为 const int*
情况2. 加了引用
也就是T&的形式:
// 模板函数:
template <typename T>
f(T& t) {...}
// auto声明:
auto& x = ...;
这种情况则需要保持原有的const,举例来看:
const int a = 1;
f(a); // T 推导为 const int,所以形参t类型为 const int&
auto& x = a; // x类型推导为 const int
保留const的原因也很好理解,因为这里是引用传递,丢掉const会导致可以修改原来的值,同样编译不通过:
const int a = 1;
int& x = a; // 编译不通过
const int& x = a; // OK
情况2.2 万能引用
如果加了两个引用符号 &&,则可以根据情况推导为普通的引用,或者右值引用,因此也叫做万能引用:
// 模板函数:
template <typename T>
f(T&& t) {...} // &&表示万能引用
// auto声明:
auto&& x = ...; // &&表示万能引用
// 举例:
int a = 1;
auto&& x = a; // x的类型是 int&
auto&& x = 1; // x的类型是 int&& 右值引用
其他情况
经过上面的分析,我们得出结论,对于模板类型的推导,其实只要能替换成普通函数,并去掉不必要的const和引用&,就可以得到推导结果。
书中特别提到了数组作为函数实参的特殊情况,但实际上这并不算什么特殊情况。我们先了解下普通函数,数组作为参数的情况。
在普通函数签名中,形参可以写成数组的形式,但实际上类型还是指针。二者最大的区别是:指针并不会包含数组长度信息。这也是为什么函数以数组作为参数时,一般还要再传入数组长度参数。
int getlen(char a[]) { // 等价于 int* a 或 int a[1]
return sizeof(a);
}
int main() {
char a[2];
cout << sizeof(a); // 输出 2 即两个char所占的空间
cout << getlen(a); // 输出 8 即int*指针所占的空间
}
其实,如果想要保留数组的长度信息,也有办法,那就是使用“数组的引用”作为函数参数:
int getlen(char (&a)[2]) { // a是数组char[2] 的引用类型
return sizeof(a); // 返回2
}
注意,形参中需指定长度,且需要与实参的数组长度一致。
而对于模板函数其实是一样的,可以使用数组作为参数,这样实际上的类型是指针;也可以使用“数组的引用”作为参数,保留长度信息。
template<typename T>
int f(T t) {
return sizeof (t);
}
template<typename T>
int g(T& t) {
return sizeof(t);
}
char a[2];
cout << f(a) << endl; // 输出 8, 即指针的长度,t被推导为char*类型
cout << g(a) << endl; // 输出 2, t被推导为 char(&)[2] 类型
auto x = a; // x类型推导为 char*
auto& y = a; // y类型推导为 char(&)[2]
仔细对比后发现,其实这和普通函数的规则完全一致,即:数组会退化为指针,数组引用则不会。
有趣的是,利用这一特性,我们可以写一个函数,获得数据长度:
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T(&)[N]) {
return N;
}
char a[2];
// 声明一个与数组a长度相同的array
std::array<char, arraySize(a)> arr{};
arraySize声明中的constexpr表示:返回的N是编译期常量。这样才可以用在array的声明中。
auto与模板唯一的区别
前面的每个例子中,都同时包含了模板与auto的类型推导,它们二者的推导结果都一摸一样。二者唯一的一点不同是:auto支持初始化表达式,而模板不支持:
template<typename T>
void f(T param);
f({1,2,3}); // 错误,无法推导
auto x = {1,2,3}; // OK,x类型推导为std::initializer_list<int>
auto y = {1,2,3.0}; // 错误,包含int和double,无法推导
int[] z = {1,2,3}; // OK
在上例中,模板与auto出现区别的根本原因在于,C++11中为了支持统一初始化,可以用这种语法进行声明;而函数参数则没有这样的语法。
当然,这样推导的结果总是std::initializer_list<T>的类型,往往不是我们想要的结果,所以还是尽量去避免这样使用。
auto使用的优劣
大多数时候,优先使用auto
使用auto的好处包括:
- 简化一长串复杂的类型
- 避免忘记初始化(使用auto未初始化会产生编译错误)
- 类型变更时可以自适应
- 能够为lambda表达式声明变量,且优于std::function,不需要堆内存
- 避免由于类型写错导致额外的运行开销
关于最后一点,额外的运行开销,举例说明一下:
std::unordered_map<std::string, int> m;
// 不使用auto进行遍历,代码出现瑕疵:
for (const std::pair<std::string, int>& p : m) {
//... 对p进行操作
}
// 使用auto:
for (const auto& p : m) {
// ...
}
在for循环中,我们本意并不希望发生任何拷贝,因此使用了const引用的形式声明p,但很可惜,在代码运行中,仍然会发生拷贝。为什么呢?
原因是,m中元素正确类型是:std::pair<const std::string, int>,const不可以丢掉。由于这和代码中p的类型不匹配,编译器就会将其拷贝一份,并进行类型转换。所以,由于程序员对p类型的判断失误,导致运行期额外的性能开销。
而使用auto时,则永远会推断出正确的类型,可以完全避免这个问题。
auto使用的坑
有一种情况,使用auto无法得到我们想要的类型,这甚至可能会导致十分隐蔽的问题。例如:
std::vector<bool> GetVec() {
return {true};
}
void HandleBool(bool b) {
cout << b;
}
int main() {
auto b = GetVec()[0]; // 这里auto如果换成bool,一切正常
HandleBool(b);
}
本例中,我们将std::vector<bool>的operator[]操作结果赋值给b,期望变量b是bool类型,并在HandlerBool中进行打印。然而实际运行发现,打印的值往往不符合预期。
这是为什么?原因在于,b的类型并没有推断为bool,而是一个std::vector<bool>::reference类型的复杂对象,其中包含一个指针,指向vector<bool>中的某个位置。
之所以这样做,是因为vector内部针对bool类型做了一些优化。正常情况下,这个对象可以正确转化成bool类型。但是,在上例中,GetVec返回的vector在执行完这一行代码后已经销毁,b对象中的指针则成为野指针,在调用HandlerBool时,无法正确转化成bool类型。
在本例中,std::vector<bool>::reference类型我们称之为代理类,它可以模拟另一种类型。类似的,智能指针shared_ptr、unique_ptr也是代理类,它们可以模拟普通指针。但本例中的代理类更加隐蔽,我们难以察觉,才会出现上述问题。
所以,对于这种隐形代理类,需要根据情况避免使用auto来声明。
网友评论