当前位置: 首页 > news >正文

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

使用 Lambda 使代码更具表现力

  • 一、Lambda VS. 仿函数
  • 二、总结

一、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 封装在一个中间函数中。

在这里插入图片描述

http://www.lryc.cn/news/352283.html

相关文章:

  • LeetCode 2644.找出可整除性得分最大的整数:暴力模拟(两层循环)
  • Python列表,元组,集合,字典详解一篇搞懂
  • Postgresql源码(132)分布式行锁的原理分析
  • 前端 防抖和节流
  • C语言 | Leetcode C语言题解之第109题有序链表转换二叉搜索树
  • 【DevOps】Linux 下安装配置 Apache 服务器:打造你的专属 Web 平台
  • 23种设计模式之一————外观模式详细介绍与讲解
  • 202109青少年软件编程(Python)等级考试试卷(四级)
  • 正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-17讲 定时器按键消抖
  • 【系统架构师】-论文考点整理
  • Android Activity 设计详解
  • 国家开放大学,javaScript程序设计-形考任务-实训五:设计登录和注册页|实训六:设计简单的购物车
  • 微服务可用性之隔离
  • 设计模式——概述
  • #P0564. 数组元素查找升级版
  • 如何修改WordPress网站的域名
  • python爬虫[简易版]
  • 128天的创意之旅:从初心到成就,我的博客创作纪念日回顾
  • 前端绘制流程节点数据
  • 2024年顶级算法-黑翅鸢优化算法(BKA)-详细原理(附matlab代码)
  • Linux 内核开发 28 内核模块文件ko文件介绍
  • DDR5—新手入门学习(一)【1-5】
  • 力扣HOT100 - 138. 随机链表的复制
  • 深入分析 Android Activity (五)
  • Kubernetes 应用滚动更新
  • 五分钟”手撕“图书管理系统
  • 8个实用网站和软件,收藏起来一定不后悔~
  • 电商内卷时代,视频号小店凭借一己之力“脱颖而出”
  • 【论文笔记】| 定制化生成PuLID
  • P1638 逛画展