Skip to content

Latest commit

 

History

History
116 lines (86 loc) · 4.08 KB

File metadata and controls

116 lines (86 loc) · 4.08 KB

条款34:优先选用lambda式,而非std::bind

假设我们有个函数来设置声音警报:

using Time = std;:chrono::steady_clock::time_point;
enum class Sound { Beep, Siren, Whistle };
using Duration = std::chrono::steady_clock::duration;
void setAlarm(Time t, Sound s, Duration d);

进一步假设,在程序的某处我们想设置在一小时后发出警报并持续30秒,警报的具体声音尚未决定。这么依赖,我们可以撰写一个lambda表达式,修改 setAlarm 的接口,这个新的接口只需指定声音即可:

auto setSoundL =
    [](Sound s)
    {
        using namespace std::chrono;
        using namespace std::literals;

        setAlarm(steady_clock::now() + 1h,
                 s,
                 30s);
    };

下面是尝试使用 std::bind 实现的代码:

using namespace std::chrono;
using namespace std::literals;

auto setSoundB =
    std::bind(setAlarm,
              steady_clock::now() + 1h, // error!
              std::placeholders::_1,
              30s);

相比于lambda表达式,std::bind 的实现方式降低了代码的可读性。还有一项错误在于,在lambda表达式中, steady_clock::now() + 1hsetAlarm 被调用的时刻评估求值。但在 std::bind 中, steady_clock::now + 1h 作为 std::bind 的参数,在绑定时被求值,最终导致的结果是,警报被设定的启动时刻是在调用 std::bind 的时刻后的一个小时,而非 setAlarm 被调用的时刻后的一小时。

为了解决这个问题,就需要使用 std::bind 的延迟求值:

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

一旦对 setAlarm 实施重载,新的问题马上出现。假设有个重载版本接收第四个形参,指定警报的音量:

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

lambda表达式的版本会正常运作,重载决议会选择三参版本的 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),
              std::placeholders::_1,
              30s);

在编译器优化层面,lambda表达式版本的代码量更小,结构更简洁,编译器可能会将其内联,而 std::bind 的内联优化可能较小。

std::bind 的灵活性也较低,返回的结果对象里,绑定的变量是按值存储的;调用绑定对象时,形参是按引用传递的,而绑定对象的函数调用运算符运用了完美转发。当然,也可以采用 std::ref 的手法按引用传递绑定的变量:

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

在C++14中,根本没有 std::bind 的适当用力。而在C++11中,std::bind 仅有两个受限的场合还有使用的理由:

  • 移动捕获。 参考条款32。
  • 多态函数对象。 因为绑定对象的函数调用运算符利用了完美转发,它就可以接收任何类型的实参。在你想要绑定的对象具有一个函数调用运算符模板时,是有利用价值的。例如,给定一个类:
class PolyWidget {
public:
    template<typename T>
    void operator()(const T& param);
    ...
};

std::bind 可以采用如下方式绑定 PolyWidget 类型对象:

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

这么一来, boundPW 就可以通过任意类型的实参加以调用:

boundPW(1930);
boundPW(nullptr);
boundPW("Rose")

在C++14中,使用带有 auto 形参的lambda表达式就可以达到相同的效果:

auto boundPW = [pw](const auto&& param)
               { pw(param); };