effective_modern_c++
  • 前言
  • 1 类型推导
    • 条款1 理解模板类型推导
    • 条款2 理解auto类型推导
    • 条款3 理解decltype
    • 条款4 知道如何查看类型推导
  • 2 auto
    • 条款5 优先使用auto而非显式声明
    • 条款6 当auto推导出非预期类型时应当使用显式的类型初始化
  • 3 使用高级cpp特性
    • 条款7 创造对象时区分()和{}
    • 条款8 优先使用nullptr而不是0或者NULL
    • 条款9 优先使用声明别名而不是typedef
    • 条款10 优先使用作用域限制的enum而不是无作用域的enum
    • 条款11 优先使用delete关键字删除函数而不是private却又不实现的函数
    • 条款12 使用override关键字声明覆盖的函数
    • 条款13 优先使用const_iterator而不是iterator
    • 条款14 如果函数不抛出异常请使用noexcept
    • 条款15 尽可能使用constexpr
    • 条款16 让const成员函数线程安全
    • 条款17 理解特殊成员函数的生成
  • 4 智能指针
    • 条款18 对于独占资源使用std::unique_ptr
    • 条款19 对于共享资源使用std::shared_ptr
    • 条款20 当std::shared_ptr可能悬空时使用std::weak_ptr
    • 条款21 优先考虑使用std::make_unique和std::make_shared,而非直接使用new
    • 条款22 当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
  • 5 右值引用和完美转发
    • 条款23 理解std::move和std::forward
    • 条款24 区分通用引用和右值引用
    • 条款25 对右值引用使用std::move,对通用引用使用std::forward
    • 条款26 避免在通用引用上重载
    • 条款27 熟悉通用引用重载的替代方法
    • 条款28 理解引用折叠
    • 条款29 假定移动操作不存在,成本高,未被使用
    • 条款30 熟悉完美转发失败的情况
  • 6 lambda表达式
    • 条款31 避免使用默认捕获模式
    • 条款32 使用初始化捕获来移动对象到闭包中
    • 条款33 对auto&&形参使用decltype以便std::forward它们
    • 条款34 考虑lambda而非std::bind
  • 7 并发api
    • 条款35 优先考虑基于任务的编程而非基于线程的编程
    • 条款36 如果有异步的必要请指定std::async::launch
    • 条款37 使std::thread在所有路径最后都不可结合
    • 条款38 关注不同线程句柄的析构行为
    • 条款39 对于一次性事件通信考虑使用void的futures
    • 条款40 对于并发使用std::atomic,对于特殊内存使用volatile
  • 8 些许调整
    • 条款41 对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
    • 条款42 考虑使用置入代替插入
Powered by GitBook
On this page
  1. 2 auto

条款5 优先使用auto而非显式声明

使用显式类型声明有如下潜在的问题,举个栗子:

int x;

容易写出上面这样的代码——忘记初始化x,因此它的值是无法确定的。也许它会被初始化为0。

再举个栗子:

template<typename It>
void dwim(It b, It e) {
    while (b != e) {
        typename std::iterator_traits<It>::value_type currValue = *b;
        ...
    }
}

需要显式指明typename std::iterator_traits<It>::value_type来表示被迭代器指向的值得类型,这并不是使用C++编程本该有的愉悦体验。

由于C++11,得益于auto,这些问题都消失了,auto变量从它们的初始化推导出其类型,所以它们必须被初始化。

int x1;  // 未初始化,且能通过编译
auto x2;  // 不能通过编译
auto x3 = 0;  // 能通过编译,运行良好

template<typename It>
void dwim(It b, It e) {
    while (b != e) {
        auto currValue = *b;
        ...
    }
}

由于auto使用类型推导,它还可以表示那些仅仅被编译器知晓的类型:

auto derefUPLess =   // comparison func.
    [](const std::unique_ptr<Widget>& p1,
       const std::unique_ptr<Widget>& p2) {  // for Widgets pointed by std::unique_ptrs
    return *p1 < *p2;
}

在C++14中,模板被进一步丢弃,因为使用lambda表达式的参数可以包含auto:

auto derefLess =   // C++14 comparison func.
    [](const auto& p1,
       const auto& p2) {  // for values pointed
    return *p1 < *p2;

也许你在想,我们不需要使用auto去声明一个持有封装体的变量,因为我们可以使用一个std::function对象。

std::function是C++11标准库的一个模板,它可以使函数指针普通化。鉴于函数指针只能指向一个函数,然而,std::function对象可以应用任何可以被调用的对象,就像函数。声明一个名为func的std::function对象,它可以引用有如下特点的可调用对象:

bool(const std::unique_ptr<Widget>&,
     const std::unique_ptr<Widget>&)  // C++11 signature for std::unique_ptr<Widget> comparsion func

你可以这么写:

std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)> func;

因为lambda表达式得到一个可调用对象,封装体可以存储在std::function对象里面。这意味着,我们可以声明不使用auto的C++11版本的derefUPLess如下:

std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)>
    derefUPLess = [](const std::unique_ptr<Widget>& p1,
                     const std::unique_ptr<Widget>& p2) {
                        return *p1 < *p2;
                    }

使用std::function和使用auto并不一样。一个使用auto声明持有一个封装的变量和封装体有同样的类型,也仅使用和封装同样大小的内存。持有一个封装体的被std::function声明的变量的类型是std::function模板的一个实例,并且对任何类型只有一个固定大小。这个内存可能不能满足封装体的需求。出现这种情况时,std::function将会开辟堆空间来存储这个封装体。导致的结果就是std::function对象一般会比auto声明的对象使用更多的内存。由于实现细节中,约束inline的使用和提供间接函数的调用,通过std::function对象来调用一个封装体比通过auto对象要慢。换言之,std::function方法通常体积比auto大,且慢,还有可能导致内存不足的异常。

auto的优点除了可以避免未初始化的变量,变量声明引起的歧义,直接持有封装体的能力。还有一个就是可以避免“类型截断”的问题。举个栗子:

std::vector<int> v;
...
unsigned sz = v.size();

v.size()定义的返回类型是std::vector<int>::size_type,但是很少有开发者对此十分清楚。std::vector<int>::size_type被指定为一个非符号的整数类型,因此很多程序员认为unsigned类型是足够的,然后写出了上面的代码。这将导致一些有趣的后果。比如说在32位windows系统上,unsigned和std::vector<int>::size_type有同样的大小,但是在64位的windows上,unsigned是32位的,而std::vector<int>::size_type是64位的。这意味着上面的代码在32位windows系统上工作良好,但是在64位windows系统上有时可能不正确,当应用程序从32位移植到64位上,这就比较浪费时间了。使用auto可以保证你不必被上面的东西所困扰:

auto sz = v.size();  // sz's typs is std::vector<int>::size_type

再看如下的代码:

std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int>& p : m) {
    ...  // do something with p
}

这看上去完美合理。但是有一个问题,意识到std::unordered_map的key部分是const类型的,在哈希表中std::pair的类型不是std::pair<std::string, int>,而是std::pair<const std::string, int>。但是这不是循环体外变量p的声明类型。后果就是,编译器竭尽全力找到一种方式,把std::pair<const std::string, int>对象转化为std::pair<std::string, int>对象。这个过程将通过复制m的一个元素到一个临时对象,然后将这个临时对象和p绑定完成。在每个循环结束的时候这个临时对象将被销毁。最终这个代码的行为将会令人吃惊,因为你本来想简单的将引用p和m的每个元素绑定的。当然这种无意的类型不匹配还是可以通过auto解决:

for (const auot& p : m) {
    ...  // as before
}

归纳

  • auto变量一定要被初始化,并且对由于类型不匹配引起的兼容和效率问题有免疫力,可以简单化代码重构,一般会比显式的声明类型敲击更少的键盘

  • auto类型的变量也受限于条款2和条款6中描述的陷阱

Previous2 autoNext条款6 当auto推导出非预期类型时应当使用显式的类型初始化

Last updated 2 years ago