C++ Notes : Effective Modern C++ 笔记 —— `auto`的使用:条目 5, 6

  • 条目5:优先使用auto而不是显式类型声明
  • 条目5:当auto推导出的类型不是期望类型时应当显式的指出类型

1. 优先使用auto

应当优先使用auto去声明变量而不是显式的使用精确的类型名去声明,auto声明有以下一些好处。

  1. auto声明可以避免忘记初始化变量
int a1;        // 可以编译通过,编译器可能会给a1赋予初始值,如全局变量时初始化为0,局部变量将未定义
               // https://zh.cppreference.com/w/cpp/language/default_initialization
// auto a2;    // 编译失败,因为auto需要从值推导类型
auto a2 = 1;   // 正确,可以防止变量未初始化
  1. auto可以简化复杂的类型声明
std::vector<int> iVec{1, 2, 3};

std::vector<int>::iterator it1 = iVec.begin();  // 复杂
auto it2 = iVec.begin();                        // 简单
  1. 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声明占用更大的内存空间,调用时间更慢且有内存溢出的风险。不展开

  1. 使用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_mapkey

// 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 &lts, 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>::referencebool的隐式转换,创建了临时变量。

再来看书上的例子:

// 假定返回值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推导出需要的类型。
    相关代码

posted on 2018-08-04 01:06  meow1234  阅读(261)  评论(0编辑  收藏  举报

导航