现代C++ 如何使用 Lambda 使代码更具表现力、更容易理解?

使用 Lambda 使代码更具表现力

一、Lambda VS. 仿函数

Lambda 是 C++11 中最引人注目的语言特性之一。它是一个强大的工具,但必须正确使用才能使代码更具表现力,而不是更难理解。

首先,要明确的是,Lambda 并没有为语言添加新的功能。任何可以用 Lambda 完成的事情,都可以用仿函数(Functor)来完成,虽然仿函数的语法更繁琐,需要更多的类型声明。

例如,比较检查一个整数集合中所有元素是否都在两个整数 a 和 b 之间的两种方式:

  • 仿函数。
  • Lambda 表达式。

仿函数版本:

class IsBetween
{
public:
    IsBetween(int a, int b) : a_(a), b_(b) {}
    bool operator()(int x) { return a_ <= x && x <= b_; }
private:
    int a_;
    int b_;
};

bool allBetweenAandB = std::all_of(numbers.begin(), numbers.end(), IsBetween(a, b));

Lambda 版本:

bool allBetweenAandB = std::all_of(numbers.begin(), numbers.end(),
           [a,b](int x) { return a <= x && x <= b; });

很明显,Lambda 版本更简洁,更易于编写,这可能是 Lambda 在 C++ 中备受关注的原因。

对于像检查一个数字是否在两个边界之间这样简单的操作,许多人可能会同意 Lambda 是更好的选择。但也并非所有情况下都是如此。

除了编写和简洁性之外,在前面的例子中,Lambda 和仿函数之间的两个主要区别是:

  • Lambda 没有名字。
  • Lambda 不隐藏其代码,而是直接在调用点展示。

但是,通过调用具有有意义名称的函数将代码从调用点移出,是管理抽象级别的一种基本技巧。但是,上面的例子是可以接受的,因为这两个表达式:

IsBetween(a, b)

[a,b](int x) { return a <= x && x <= b; }

读起来很相似。它们的抽象级别是一致的。

但是,当代码变得更加复杂时,结果就会大不相同,以下例子将说明这一点。

一个表示盒子的类的例子,它可以根据尺寸和材质(金属、塑料、木材等)进行构建,并提供对盒子特性的访问:

class Box
{
public:
    Box(double length, double width, double height, Material material);
    double getVolume() const;
    double getSidesSurface() const;
    Material getMaterial() const;
private:
    double length_;
    double width_;
    double height_;
    Material material_;
};

有一个这样的盒子集合:

std::vector<Box> boxes = ....

想要选择能够安全地容纳某种产品(水、油、果汁等)的盒子。

通过一些物理推理,可以近似地将产品对盒子四个侧面的压力视为产品的重量,它分布在这些侧面的表面上。如果材料能够承受施加的压力,则盒子足够坚固。

假设材料可以承受的最大压力为:

class Material
{
public:
    double getMaxPressure() const;
    ....
};

产品提供了它的密度,以便计算它的重量:

class Product
{
public:
    double getDensity() const;
    ....
};

现在,要选择能够安全地容纳产品 product 的盒子,可以使用 STL 和 Lambda 编写以下代码:

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes),
    [product](const Box& box)
    {
        const double volume = box.getVolume();
        const double weight = volume * product.getDensity();
        const double sidesSurface = box.getSidesSurface();
        const double pressure = weight / sidesSurface;
        const double maxPressure = box.getMaterial().getMaxPressure();
        return pressure <= maxPressure;
    });

以下是等效的仿函数定义:

class Resists
{
public:
    explicit Resists(const Product& product) : product_(product) {}
    bool operator()(const Box& box)
    {
        const double volume = box.getVolume();
        const double weight = volume * product_.getDensity();
        const double sidesSurface = box.getSidesSurface();
        const double pressure = weight / sidesSurface;
        const double maxPressure = box.getMaterial().getMaxPressure();
        return pressure <= maxPressure;
    }
private:
    Product product_;
};

在主代码中:

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes), Resists(product));

尽管仿函数仍然需要更多的类型声明,但使用仿函数的算法代码行看起来比使用 Lambda 更清晰。不幸的是,对于 Lambda 版本来说,这一行代码更重要,因为它是主要代码。

在这里,Lambda 的问题在于它展示了如何进行盒子检查,而不是简单地说检查已经完成,因此它的抽象级别太低了。在该示例中,它会影响代码的可读性,因为它迫使读者深入 Lambda 的主体以弄清楚它做了什么,而不是简单地说明它做了什么。

在这里,有必要将代码从调用点隐藏,并为它赋予一个有意义的名称。仿函数在这方面做得更好。

但这是否意味着不应该在任何非平凡的情况下使用 Lambda?当然不是。

Lambda 被设计得比仿函数更轻便、更方便,同时仍然保持抽象级别有序。这里的技巧是通过使用中间函数将 Lambda 的代码隐藏在一个有意义的名称后面。以下是 C++14 中实现此目的的方法:

auto resists(const Product& product)
{
    return [product](const Box& box)
    {
        const double volume = box.getVolume();
        const double weight = volume * product.getDensity();
        const double sidesSurface = box.getSidesSurface();
        const double pressure = weight / sidesSurface;
        const double maxPressure = box.getMaterial().getMaxPressure();
        return pressure <= maxPressure;
    };
}

在这里,Lambda 被封装在一个函数中,该函数只是创建它并返回它。这个函数的作用是将 Lambda 隐藏在一个有意义的名称后面。

以下是主代码,它从实现负担中解脱出来:

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes), resists(product));

现在,为了使代码更具表现力,在本文的其余部分使用范围(Range)而不是 STL 迭代器:

auto goodBoxes = boxes | ranges::view::filter(resists(product));

当调用算法周围有其他代码时,隐藏实现的必要性变得更加重要。为了说明这一点,添加一个要求,即盒子必须从用逗号分隔的文本测量描述(例如,“16,12.2,5”)和所有盒子的唯一材料进行初始化。

如果直接调用即时 Lambda,结果将如下所示:

auto goodBoxes = boxesDescriptions
  | ranges::view::transform([material](std::string const& textualDescription)
    {
        std::vector<std::string> strSizes;
        boost::split(strSizes, textualDescription, [](char c){ return c == ','; });
        const auto sizes = strSizes | ranges::view::transform([](const std::string& s) {return std::stod(s); });
        if (sizes.size() != 3) throw InvalidBoxDescription(textualDescription);
        return Box(sizes[0], sizes[1], sizes[2], material);
    })
  | ranges::view::filter([product](Box const& box)
    {
        const double volume = box.getVolume();
        const double weight = volume * product.getDensity();
        const double sidesSurface = box.getSidesSurface();
        const double pressure = weight / sidesSurface;
        const double maxPressure = box.getMaterial().getMaxPressure();
        return pressure <= maxPressure;
    });

这变得非常难以阅读。但是,通过使用中间函数来封装 Lambda,代码将变成:

auto goodBoxes = textualDescriptions | ranges::view::transform(createBox(material))
                                     | ranges::view::filter(resists(product));

这才是希望代码呈现的样子。

请注意,这种技术在 C++14 中有效,但在 C++11 中略有不同。

Lambda 的类型没有在标准中指定,而是由编译器的实现决定。这里,auto 作为返回值类型允许编译器将函数的返回值类型写为 Lambda 的类型。但在 C++11 中,不能这样做,因此需要指定一些返回值类型。Lambda 可以隐式转换为具有正确类型参数的 std::function,并且可以在 STL 和范围算法中使用。请注意,std::function 会带来与堆分配和虚拟调用间接相关的额外成本。

在 C++11 中,resists 函数的建议代码将是:

std::function<bool(const Box&)> resists(const Product& product)
{
    return [product](const Box& box)
    {
        const double volume = box.getVolume();
        const double weight = volume * product.getDensity();
        const double sidesSurface = box.getSidesSurface();
        const double pressure = weight / sidesSurface;
        const double maxPressure = box.getMaterial().getMaxPressure();
        return pressure <= maxPressure;
    };
}

请注意,在 C++11 和 C++14 的实现中,resists 函数返回的 Lambda 可能不会被复制,因为返回值优化可能会优化掉它。还要注意,返回 auto 的函数必须在其调用点可见。因此,这种技术最适合在与调用代码相同的文件中定义的 Lambda。

二、总结

  • 对于对抽象级别透明的函数,请使用在调用点定义的匿名 Lambda。
  • 否则,将 Lambda 封装在一个中间函数中。

在这里插入图片描述

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值