Lambda可以说是C ++ 11语言中最著名的功能之一。 它是一种有用的工具,但必须确保正确使用它们,以使代码更具表现力,而不是晦涩难懂。
首先,让我们明确一点,lambda不会为语言添加功能。 使用lambda可以执行的所有操作都可以使用函子来完成,尽管语法更繁重且要敲更多代码。
例如,这是检查一个int集合的所有元素是否包含在另外两个int a和b之间的比较示例:
函子版本:
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版本更简洁,更容易键入,这可能可以解释为什么大肆宣传C ++中加入了lambda。
对于检查数字是否在两个边界之间这样的简单处理,我想许多人都同意应优先选择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;
....
};
现在要选择足以容纳产品的盒子,我们可以使用带有lambda的STL编写以下代码:
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相比,使用算法库的这一行应该清晰得多。不幸的是,对于lambdas版本,这一行更为重要(但不清晰),因为它是主要代码,你和其他开发人员需要读这些代码来了解它做了什么。
在这里,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和range算法中使用。 请注意,正如Antoine在评论部分中指出的那样,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封装在中介函数中。