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. 6 lambda表达式

条款34 考虑lambda而非std::bind

C++11中的std::bind是C++98的std::bind1st和std::bind2nd的后续,但在2005年已经非正式成为了标准库的一部分。那时标准化委员采用了TR1的文档,其中包含了bind的规范。(在TR1中,bind位于不同的命名空间,因此它是std::tr1::bind,而不是std::bind,接口细节也有所不同)。这段历史意味着一些程序员有十年及以上的std::bind使用经验。如果你是其中之一,可能会不愿意放弃一个对你有用的工具。这是可以理解的,但是在这种情况下,改变是更好的,因为在C++11中,lambda几乎总是比std::bind更好的选择。从C++14开始,lambda的作用不仅强大,而且是完全值得使用的。

这个条款假设你熟悉std::bind。如果不是这样,你将需要获得基本的了解,然后再继续。无论如何,这样的理解都是值得的,因为你永远不知道何时会在阅读或维护的代码库中遇到std::bind。

与条款32中一样,我们将从std::bind返回的函数对象称为bind对象(bind objects)。

优先lambda而不是std::bind的最重要原因是lambda更易读。例如,假设我们有一个设置警报器的函数:

using Time = std::chrono::steady_clock::time_point;

enum class Sound {Beep, Siren, Whistle};

using Duration = std::chrono::steady_clock::duration;

// 在时间t,使用s声音响铃时长d
void setAlarm(Time t, Sound s, Duration d);

进一步假设,在程序的某个时刻,我们已经确定需要设置一个小时后响30秒的警报器。但是,具体声音仍未确定。我们可以编写一个lambda来修改setAlarm的界面,以便仅需要指定声音:

auto setSoundL = [](Sound s) {  // setSoundL(“L”指代“lambda”)是个函数对象
    using namespace std::chrono;

    setAlarm(steady_clock::now() + hours(1),  // setAlarm三行高亮
             s,
             seconds(30));
};

我们在lambda中高亮了对setAlarm的调用。这看来起是一个很正常的函数调用,即使是几乎没有lambda经验的读者也可以看到:传递给lambda的形参s又作为实参被传递给了setAlarm。

我们通过使用标准后缀如秒(s),毫秒(ms)和小时(h)等简化在C++14中的代码,其中标准后缀基于C++11对用户自定义常量的支持。这些后缀在std::literals命名空间中实现,因此上述代码可以按照以下方式重写:

auto setSoundL = [](Sound s) {
    using namespace std::chrono;
    using namespace std::literals;  // 对于C++14后缀

    setAlarm(steady_clock::now + 1h,  // C++14写法,但是含义同上
             s,
             30s);
};

下面是我们第一次编写对应的std::bind调用。这里存在一个我们后续会修复的错误,但正确的代码会更加复杂,即使是此简化版本也会凸显一些重要问题:

using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;  // “_1”使用需要

auto setSoundB = std::bind(  // “B”代表“bind”
    setAlarm,
    steady_clock()::now + 1h,  // 不正确!见下
    _1,
    30s
);

我想像在之前的lambda中一样高亮对setAlarm的调用,但是没这么个调用让我高亮。这段代码的读者只需知道,调用setSoundB会使用在对std::bind的调用中所指定的时间和持续时间来调用setAlarm。对于门外汉来说,占位符“_1”完全是一个魔法,但即使是知情的读者也必须从思维上将占位符中的数字映射到其在std::bind形参列表中的位置,以便明白调用setSoundB时的第一个实参会被传递进setAlarm,作为调用setAlarm的第二个实参。在对std::bind的调用中未标识此实参的类型,因此读者必须查阅setAlarm声明以确定将哪种实参传递给setSoundB。

但正如我所说,代码并不完全正确。在lambda中,表达式steady_clock::now() + 1h显然是setAlarm的实参。调用setAlarm时将对其进行计算。可以理解:我们希望在调用setAlarm后一小时响铃。但是,在std::bind调用中,将steady_clock::now() + 1h作为实参传递给了std::bind,而不是setAlarm。这意味着将在调用std::bind时对表达式进行求值,并且该表达式产生的时间将存储在产生的bind对象中。结果,警报器将被设置为在调用std::bind后一小时发出声音,而不是在调用setAlarm一小时后发出。

要解决此问题,需要告诉std::bind推迟对表达式的求值,直到调用setAlarm为止,而这样做的方法是将对std::bind的第二个调用嵌套在第一个调用中:

auto setSoundB = std::bind(
    setAlarm,
    std::bind(std::plus<>(), std::bind(steady_clock::now), 1h),
    _1,
    30s
);

如果你熟悉C++98的std::plus模板,你可能会惊讶地发现在此代码中,尖括号之间未指定任何类型,即该代码包含“std::plus<>”,而不是“std::plus<type>”。 在C++14中,通常可以省略标准运算符模板的模板类型实参,因此无需在此处提供。C++11没有提供此类功能,因此等效于lambda的C++11 std::bind为:

using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB = std::bind(
    setAlarm,
    std::bind(std::plus<steady_clock::time_point>(), std::bind(steady_clock::now), hours(1)),
    _1,
    seconds(30)
);

如果此时lambda看起来还没有吸引力,那么应该检查一下视力了。

当setAlarm重载时,会出现一个新问题。假设有一个重载函数,其中第四个形参指定了音量:

enum class Volume {Normal, Loud, LoudPlusPlus};

void setAlarm(Time t, Sound s, Duration d, Volume v);

lambda能继续像以前一样使用,因为根据重载规则选择了setAlarm的三实参版本:

auto setSoundL = [](Sound s) {  // 和之前一样
    using namespace std::chrono;
    setAlarm(steady_clock::now() + 1h, s, 30s);  // 可以,调用三实参版本的setAlarm
};

然而,std::bind的调用将会编译失败:

auto setSoundB = std::bind(
    setAlarm,  // 错误!哪个setAlarm?
    std::bind(std::plus<>(), steady_clock::now(), 1h),
    _1,
    30s
);

这里的问题是,编译器无法确定应将两个setAlarm函数中的哪一个传递给std::bind。它们仅有的是一个函数名称,而这个单一个函数名称是有歧义的。

要使对std::bind的调用能编译,必须将setAlarm强制转换为适当的函数指针类型:

using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);

auto setSoundB = std::bind(  // 现在可以了
    static_cast<SetAlarm3ParamType>(setAlarm),
    std::bind(std::plus<>(), steady_clock::now(), 1h),
    _1,
    30s
);

但这在lambda和std::bind的使用上带来了另一个区别。在setSoundL的函数调用操作符(即lambda的闭包类对应的函数调用操作符)内部,对setAlarm的调用是正常的函数调用,编译器可以按常规方式进行内联:

setSoundL(Sound::Siren);  // setAlarm函数体在这可以很好地内联

但是,对std::bind的调用是将函数指针传递给setAlarm,这意味着在setSoundB的函数调用操作符(即绑定对象的函数调用操作符)内部,对setAlarm的调用是通过一个函数指针。编译器不太可能通过函数指针内联函数,这意味着与通过setSoundL进行调用相比,通过setSoundB对setAlarm的调用,其函数不大可能被内联:

setSoundB(Sound::Siren);  // setAlarm函数体在这不太可能内联

因此,使用lambda可能会比使用std::bind能生成更快的代码。

setAlarm示例仅涉及一个简单的函数调用。如果你想做更复杂的事情,使用lambda会更有利。例如,考虑以下C++14的lambda使用,它返回其实参是否在最小值(lowVal)和最大值(highVal)之间的结果,其中lowVal和highVal是局部变量:

auto betweenL = [lowVal, highVal](const auto& val) {  // C++14
    return lowVal <= val && val <= highVal;
}

使用std::bind可以表达相同的内容,但是该构造是一个通过晦涩难懂的代码来保证工作安全性的示例:

using namespace std::placeholders;

auto betweenB = std::bind(
    std::logic_and<>(),  // C++14
    std::bind(std::less_equal<>(), lowVal, _1),
    std::bind(std::less_equal<>(), _1, highVal)
);

在C++11中,我们必须指定要比较的类型,然后std::bind调用将如下所示:

auto betweenB = std::bind(
    std::logic_and<bool>(),  // C++11版本
    std::bind(std::less_equal<int>(), lowVal, _1),
    std::bind(std::less_equal<int>(), _1, highVal)
);

当然,在C++11中,lambda也不能采用auto形参,因此它也必须指定一个类型:

auto betweenL = [lowVal, highVal](int val) {  // C++11版本
    return lowVal <= val && val <= highVal;
};

无论哪种方式,我希望我们都能同意,lambda版本不仅更短,而且更易于理解和维护。

之前我就说过,对于那些没有std::bind使用经验的人,其占位符(例如_1,_2等)都是魔法。但是这不仅仅在于占位符的行为是不透明的。假设我们有一个函数可以创建Widget的压缩副本,

enum class CompLevel {Low, Normal, High};  // 压缩等级

Widget compress(const Widget& w, CompLevel lev);  // 制作w的压缩副本

并且我们想创建一个函数对象,该函数对象允许我们指定Widget w的压缩级别。这种使用std::bind的话将创建一个这样的对象:

Widget w;
using namespace std::placeholders;

auto compressRateB = std::bind(compress, w, _1);

现在,当我们将w传递给std::bind时,必须将其存储起来,以便以后进行压缩。它存储在对象compressRateB中,但是它是如何被存储的呢——是通过值还是引用?之所以会有所不同,是因为如果在对std::bind的调用与对compressRateB的调用之间修改了w,则按引用捕获的w将反映这个更改,而按值捕获则不会。

答案是它是按值捕获的(std::bind总是拷贝它的实参,但是调用者可以使用引用来存储实参,这要通过应用std::ref到实参上实现。auto compressRateB = std::bind(compress, std::ref(w), _1);的结果就是compressRateB行为像是持有w的引用而非副本。),但唯一知道的方法是记住std::bind的工作方式;在对std::bind的调用中没有任何迹象。然而在lambda方法中,其中w是通过值还是通过引用捕获是显式的:

auto compressRateL = [w](CompLevel lev) {  // w是按值捕获,lev是按值传递
    return compress(w, lev);
};

同样明确的是形参是如何传递给lambda的。在这里,很明显形参lev是通过值传递的。因此:

compressRateL(CompLevel::High);  // 实参按值传递

但是在对由std::bind生成的对象调用中,实参如何传递?

compressRateB(CompLevel::High);  // 实参如何传递?

同样,唯一的方法是记住std::bind的工作方式。(答案是传递给bind对象的所有实参都是通过引用传递的,因为此类对象的函数调用运算符使用完美转发。)

与lambda相比,使用std::bind进行编码的代码可读性较低,表达能力较低,并且效率可能较低。在C++14中,没有std::bind的合理用例。但是,在C++11中,可以在两个受约束的情况下证明使用std::bind是合理的:

  • 移动捕获。C++11的lambda不提供移动捕获,但是可以通过结合lambda和std::bind来模拟。有关详细信息,请参阅条款32,该条款还解释了在C++14中,lambda对初始化捕获的支持消除了这个模拟的需求

  • 多态函数对象。因为bind对象上的函数调用运算符使用完美转发,所以它可以接受任何类型的实参(以条款30中描述的完美转发的限制为界限)。当你要绑定带有模板化函数调用运算符的对象时,此功能很有用。 例如这个类,

class PolyWidget {
public:
    template<typename T>
    void operator()(const T& param);
    ...
};

std::bind可以如下绑定一个PolyWidget对象:

PolyWidget w;
auto boundPW = std::bind(pw, _1);

boundPW可以接受任意类型的对象了:

boundPW(1930);  // 传int给PolyWidget::operator()
boundPW(nullptr);  // 传nullptr给PolyWidget::operator()
boundPW("Rosebud");  // 传字面值给PolyWidget::operator()

这一点无法使用C++11的lambda做到。但是,在C++14中,可以通过带有auto形参的lambda轻松实现:

auto boundPW = [pw](const auto& param) {  // C++14
    pw(param);
};

当然,这些是特殊情况,并且是暂时的特殊情况,因为支持C++14 lambda的编译器越来越普遍了。

当bind在2005年被非正式地添加到C++中时,与1998年的前身相比有了很大的改进。在C++11中增加了lambda支持,这使得std::bind几乎已经过时了,从C++14开始,更是没有很好的用例了。

归纳

  • 与使用std::bind相比,lambda更易读,更具表达力并且可能更高效

  • 只有在C++11中,std::bind可能对实现移动捕获或绑定带有模板化函数调用运算符的对象时会很有用

Previous条款33 对auto&&形参使用decltype以便std::forward它们Next7 并发api

Last updated 2 years ago