Item 34: Prefer lambdas to std::bind.

Item 34: Prefer lambdas to std::bind.

Effective Modern C++ Item 34 的学习和解读。

C++11 的 std::bind 是对 C++98 std:bind1st 和 std::bind2nd 的继承,它在 2005 年以 TR1 文档形式非正式地成为标准库的一部分。因为,许多 C++ 程序员可能有十几年的 std::bind 使用经验,现在告诉他放弃使用 std::bind,多少可能有些不情愿。但是,本 Item 会告诉你使用 lambda 替代 std::bind 将是个更好的选择。

对于 C++11,除了个别边缘 case,lambda 表达式要比 std::bind 更有优势。而对于 C++14,lambda 则可以完全替代 std::bind。

lambda 第一个优势是代码的可读性更强。例如,我们有一个设置声音报警的函数:

// typedef for a point in time (see Item 9 for syntax)
using Time = std::chrono::steady_clock::time_point;
// see Item 10 for "enum class"
enum class Sound { Beep, Siren, Whistle };
// typedef for a length of time
using Duration = std::chrono::steady_clock::duration;
// at time t, make sound s for duration d
void setAlarm(Time t, Sound s, Duration d);

如果我们想在设置声音报警后 1h,关闭报警,并持续 30s。使用 lambda 表达式修正 setAlarm,可以实现如下:

// setSoundL ("L" for "lambda") is a function object allowing a
// sound to be specified for a 30-sec alarm to go off an hour
// after it's set
auto setSoundL =
  [](Sound s)
  {
    // make std::chrono components available w/o qualification
    using namespace std::chrono;
    setAlarm(steady_clock::now() + hours(1), // alarm to go off
             s, // in an hour for
             seconds(30)); // 30 seconds
  };

上述代码逻辑非常清楚。如果使用 C++14 字面值 std::literals 改写上面代码,可以更加简洁:

auto setSoundL =
  [](Sound s)
  {
    using namespace std::chrono;
    using namespace std::literals; // for C++14 suffixes
    setAlarm(steady_clock::now() + 1h, // C++14, but
             s, // same meaning
             30s); // as above
  };

如果使用 std::bind 直接替换 lambda 表达式,可以改写成如下:

using namespace std::chrono; // as above
using namespace std::literals;
using namespace std::placeholders; // needed for use of "_1"
auto setSoundB = // "B" for "bind"
  std::bind(setAlarm,
            steady_clock::now() + 1h, // incorrect! see below
            _1,
            30s);

首先,相较于 lambda 版本,使用 std::bind,函数调用和传参不那么明显。并且这里还有一个占位符 “_1”,使用 setSoundB 时候,你需要查阅 setAlarm 的函数申明,才知道这里的占位符的传参类型。

最重要的是这里的代码逻辑有问题。显然,我们期望的是在调用 setAlarm 时候计算表达式 steady_clock::now() + 1h 的值。但是,使用 std::bind 的时候,表达式 steady_clock::now() + 1h 是传递给 std::bind 而不是 setAlarm,这意味着,在调用 std::bind 的时候,表达式的值就被计算出来,然后保存在绑定对象内部。这就导致和在调用 setAlarm 时候计算表达式的期望不一致。可以再使用一个 std::bind 封装该表达式以延迟到 setAlarm 调用的时候才计算:

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

注意到 std::plus<> 缺省了类型参数,这是 C++14 的新特性,如果是 C++11,则需要指定类型:

auto setSoundB =
  std::bind(setAlarm,
            std::bind(std::plus<steady_clock::time_point>(), // C++11
                      steady_clock::now(),
                      hours(1)),
            _1,
            seconds(30));

如果 setAlarm 增加一个重载版本:

enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);

先前 lambda 版本代码依然可以正常工作。但是,std::bind 将会产生编译报错。因为编译器无法确认传递哪个版本的 setAlarm。需要将 setAlarm 转换为合适的函数指针:

using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB =                                        // now
  std::bind(static_cast<SetAlarm3ParamType>(setAlarm),  // okay
            std::bind(std::plus<>(),
                      steady_clock::now(),
                      1h),
            _1,
            30s);

但是,这又引入了 std::bind 和 lambda 二者的不同。setSoundL 使用正常的函数调用来调用 setAlarm,编译器可以选择使用内联。

setSoundL(Sound::Siren); // body of setAlarm may
                         // well be inlined here

但是 std::bind 不可以,setSoundB 使用函数指针调用调用 setAlarm,这是运行期的行为,无法被内联。

setSoundB(Sound::Siren); // body of setAlarm is less
                         // likely to be inlined here

这就是使用 lambda 的第二个优势:代码的性能可能会更好。

使用 lambda 的第三个优势是代码更容易理解。看下面的例子:

enum class CompLevel { Low, Normal, High }; // compression level
Widget compress(const Widget& w, // make compressed
                CompLevel lev);  // copy of w

假设我们想创建一个函数对象,用来指定特定 Widget 的压缩等级。使用 std::bind 创建函数对象:

Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);

传递 w 给 std::bind,那么 w 如何存放在 compressRateB 内部的呢?是传值还是传引用?如果 w 在调用 std::bindcompressRateB 之间发生改变,传引用的方式将导致结果的不同。

std::bind 默认是拷贝它的参数到绑定对象内,用户可以使用 std::ref 指定传引用:

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

这就需要你了解 std::bind 实现机制。但对于 lambda 的实现版本,w 是值捕获还是引用捕获非常明显:

auto compressRateL =            // w is captured by
  [w](CompLevel lev)            // value; lev is
  { return compress(w, lev); }; // passed by value

同样明显的是参数如何传递给 lambda 的。这里,很明显 lev 是值传递:

compressRateL(CompLevel::High); // arg is passed
                                // by value

但是,std::bind 的绑定对象的调用,参数是如何传递的?

compressRateB(CompLevel::High); // how is arg
                                // passed?

答案是引用传递,这就需要你了解 std::bind 的工作机制:std::bind 绑定对象的函数调用使用了完美转发机制。

通过上述比较我们可以看到,相较于使用 std::bind,使用 lambda 表达式的代码可读性更强、更容易理解、性能可能更好。对于 C++14,你没有理由不选择使用 lambda。对于 C++11,只有两种场景,std::bind 可以弥补 lambda 的不足:

第一:移动捕获。C++14 的初始化捕获模式支持移动捕获。C++11 的 lambda 不支持移动捕获,可以使用 std::bind 模拟来间接实现,参见 Item32 。

第二:多态函数对象。C++14 支持 auto 参数类型,也即通用 lambda,参见 Item33 。但是 C++11 不支持通用 lambda。而 std::bind 绑定对象的函数调用使用完美转发实现,可以接收任何类型的参数。如下例子:

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

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

boundPW(1930);       // pass int to
                     // PolyWidget::operator()
boundPW(nullptr);    // pass nullptr to
                     // PolyWidget::operator()
boundPW("Rosebud");  // pass string literal to
                     // PolyWidget::operator()

C++11 做不到,C++14 则很容易:

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

最后总结下:

  • 相较于 std::bind,lambda 代码可读性更强、更容易理解、性能可能更好。
  • C++11 的 std::bind 在实现移动捕获、模板函数对象方面可以弥补 lambda 的不足。

你可能感兴趣的:(C++,Effective,Modern,C++,c++,开发语言,C++14,C++11,移动捕获)