C++ Notes : Effective Modern C++ 笔记 —— `auto`的使用:条目 5, 6
- 条目5:优先使用
auto
而不是显式类型声明 - 条目5:当
auto
推导出的类型不是期望类型时应当显式的指出类型
1. 优先使用auto
应当优先使用auto
去声明变量而不是显式的使用精确的类型名去声明,auto
声明有以下一些好处。
auto
声明可以避免忘记初始化变量
int a1; // 可以编译通过,编译器可能会给a1赋予初始值,如全局变量时初始化为0,局部变量将未定义
// https://zh.cppreference.com/w/cpp/language/default_initialization
// auto a2; // 编译失败,因为auto需要从值推导类型
auto a2 = 1; // 正确,可以防止变量未初始化
auto
可以简化复杂的类型声明
std::vector<int> iVec{1, 2, 3};
std::vector<int>::iterator it1 = iVec.begin(); // 复杂
auto it2 = iVec.begin(); // 简单
auto
还可以表示某些只有编译器才知道的类型
比如lambda表达式
auto add1 = [](const int &a, const int &b) {
return a + b;
};
// C++14起甚至连lambda表达式的形参都可以使用auto
auto add2 = [](const auto &a, const auto &b) {
return a + b;
};
虽然对于lambda表达式,也可以使用std::function
来声明(callable object),但是它本质上和lambda表达式是不同的类型,并且它比auto
声明占用更大的内存空间,调用时间更慢且有内存溢出的风险。不展开
- 使用
auto
声明有时候可以避免由于我们不清楚确切类型而盲目使用显式声明造成的问题(书中称为 type shortcuts 问题)
比如:
unsigned sz1 = iVec.size(); // sz1 类型 unsigned
auto sz2 = iVec.size(); // sz2 类型推导为 std::vector<int>::size_type
// iVec.size() 返回类型为 std::vector<int>::size_type。对于sz1,在32为系统下他们大小是一致的,但是64位系统下就不一样了。auto就不会出现这样的问题
再比如下面一个例子。
为了方便展示结果,首先定义一个帮助类用作std::unordered_map
的key
// help class defined for demonstrate `type shortcuts`
// begin help
struct TypeShortcut {
TypeShortcut(std::string s) : mString(std::move(s)) {}
void echo() const {
std::cout << mString << std::endl;
}
~TypeShortcut() {
mString = "oops! I have been destroyed";
}
std::string mString;
};
struct TypeShortcutHash {
std::size_t operator()(const TypeShortcut &ts) const {
return std::hash<std::string>{}(ts.mString);
}
};
struct TypeShortcutEqual {
bool operator()(const TypeShortcut <s, const TypeShortcut &rts) const {
return lts.mString == rts.mString;
}
};
// end help
// define an unordered_map object
std::unordered_map<TypeShortcut, int, TypeShortcutHash, TypeShortcutEqual> typeShortcutMap{{{"Hello"}, 1}, {{"World"}, 2}};
接下来看下面一段代码:
// use explicit type
const TypeShortcut* pTS1 = nullptr;
for (const std::pair<TypeShortcut, int>& ts : typeShortcutMap) {
pTS1 = &(ts.first);
std::cout << "e.g.1 - in: ";
// yes, out put in map
pTS1->echo();
}
std::cout << "e.g.1 - out: ";
// "oops! I have been destroyed"
pTS1->echo();
// 输出结果
/*
e.g.1 - in: World
e.g.1 - in: Hello
e.g.1 - out: oops! I have been destroyed
*/
发现,表面上看上去这里ts
声明为引用类型,它应该引用的是typeShortcutMap
中实际的元素,pTS1
指向其第一个元素的地址,即应当指向的是typeShortcutMap
中元素的地址。但是循环结束后,输出结果时却发现被析构了。这是因为std::unordered_map<TypeShortcut, int>
内部hashtable定义为std::pair<const TypeShortcut, int>
而不是std::pair<TypeShortcut, int>
。非const
引用不能指向const
变量,所以这里拷贝了一份std::pair<TypeShortcut, int>
类型的临时值,ts
引用指向该临时值。循环结束之后,临时值被释放,所以出现上面的结果。这是因为在不了解std::unordered_map
内部结构的情况下,使用了想当然的类型声明,为了避免这种问题,只需要不显式的声明变量类型,直接使用auto
即可,因为auto
可以推导出正确的类型。
// use auto
const TypeShortcut* pTS2 = nullptr;
for (const auto& ts : typeShortcutMap) {
pTS2 = &(ts.first);
std::cout << "e.g.2 - in: ";
// yes, output content in map
pTS2->echo();
}
std::cout << "e.g.2 - out: ";
// still output content in map
pTS2->echo();
// 输出结果
/*
e.g.2 - in: World
e.g.2 - in: Hello
e.g.2 - out: Hello
*/
2. auto
也有不好的时候
auto
也不是完美的,它也伴随着一些陷阱。一个就是item 2(前一篇笔记第2点)中提到过的,auto
在推导列表初始化值的时候存在的一些陷阱。
还有一种情况是,当类型内部存在某些看不见的代理类时,auto
推导出的结果可能不是我们所需要的类型,比如std::vector<bool>
。
auto echoBoolVector = [](const std::vector<bool>& vec) {
for(const auto& v : vec) {
std::cout << std::boolalpha << v << ", ";
}
std::cout << std::endl;
};
std::vector<bool> boolVec1{false, false, false};
std::vector<bool> boolVec2{false, false, false};
for(auto b : boolVec1) {
b = true;
}
// true, true, true,
echoBoolVector(boolVec1);
for (bool b : boolVec2) {
b = true;
}
// false, false, false
echoBoolVector(boolVec2);
第一个循环结束后,boolVec1
的元素全部变成了true
。这是因为,尽管这里使用的时auto b
,是一个值类型,但是这个值却不是bool
类型。因为std::vector<bool>
是一个特殊的容器,他是为了节省空间的一个特化版本。这里的b
得到的是一个std::vector<bool>::reference
类型(operator[]
返回值)。这是其内部的一个代理类型,尽管以值返回,但它表示的是到std::vector<bool>
里单个bool
的引用。所以修改b
会影响boolVec1
的内容。第二个循环显式使用bool
类型声明就没有问题了,这里做了std::vector<bool>::reference
到bool
的隐式转换,创建了临时变量。
再来看书上的例子:
// 假定返回值std::vector<bool> 表示 w 对各 feature 的支持情况
std::vector<bool> features(const Widget& w);
// 使用
Widget w;
bool highPriority = features(w)[5]; // 假定第五位表示 w 是否是高优先级
processWidget(w, highPriority); // 根据优先级处理 w
通过第一个例子可以知道,这里features(w)
返回一个std::vector<bool>
对象,通过运算符[]
,返回一个std::vector<bool>::reference
对象(值),它会被隐式的转换到bool
类型(新值)然后赋给highPriority
变量,highPriority
因此获得了第5
为的布尔值。好的,没问题。
那如果这里使用的是auto highPriority = features(w)[5];
呢?
依然,首先features(w)
返回一个std::vector<bool>
对象,然后通过运算符[]
返回一个std::vector<bool>::reference
对象(值)。接下来就不一样了,因为auto
可以推导出std::vector<bool>::reference
类型,highPriority
将不会直接获得第5
位的布尔值,其值是多少还取决于std::vector<bool>::reference
代理类的实现。一种实现就是它包含一个指针,指向引用位加偏移量(简单理解它指向features(w)
返回的std::vector<bool>
对象中的某个地址就行了)。然而,问题就出在features(w)
返回的std::vector<bool>
对象是一个临时对象(称其为temp
),那么std::vector<bool>::reference
就包含了一个指针指向了这个临时对象temp
的某块地址,而这里highPriority
是它的拷贝,即它也指向了这个临时对象的某块地址。语句执行完后,临时变量temp
被销毁,highPriority
就变成了悬浮指针。接下来调用processWidget(w, highPriority);
的行为就变成未定义的了。
这是一个invisible的内部代理类型的例子,代理类型不太适合使用auto
声明,应当避免下面的这种声明形式(代理类这里没有展开):
auto someVar = expression of "invisible" proxy class type;
尽管这么说,但是如何知道某个地方使用了这种代理对象却不容易。。。
当发现auto
推导出的类型不是我们期望的类型时,可以做一下类型转换,比如:
auto highPriority = static_cast<bool>(features(w)[5]);
3. 总结
auto
变量必须初始化,并且通常可以避免由于类型不匹配(不同平台)导致的移植性和效率问题,并且抑郁重构(无需到处改变量声明),并且书写简洁。(所以能用就用😀)- 但是
auto
在推导{...}
初始化时要注意 - 不可见
invisible
的代理类型有可能会导致auto
从初始化表达式推导出“错误”的类型!! - 使用显式指明初始化类型
cast
可以强制auto
推导出需要的类型。
相关代码