1. C++98/03 中的 Lambda

作为开始,了解一些关于我们所讨论的主题的背景知识是很有必要的。为此,我们会转而回顾过去,看看那些不使用任何现代 C++ 技术的代码——即 C++98/03 规范下的代码。

在本章中,我们将会学习:

  • 如何将旧式的函数对象传给 C++ 标准库中的各种算法。
  • 函数对象类型的限制。
  • 为什么函数助手不够好。
  • C++0x/C++11 中 引入 Lambda 的动机。

C++98/03 中的可调用对象

标准库的一个基本设计思想是对于像 std::sort,std::for_each,std::transform 等这样的泛型函数,能够接受任何可调用对象然后对输入容器中的每个元素依次调用它。然而,在 C++98/03 中,可调用对象只包括函数指针和重载了调用操作符的类类型(通常被称为“函子”)。

举例来说,我们有一个打印一个向量中所有元素的应用程序。
在第一个版本中,我们使用普通的函数来实现:

// Ex1_1: 一个基础的函数对象.
#include <algorithm>
#include <iostream>
#include <vector>

void PrintFunc(int x) {
    std::cout << x << '\n';
}

int main() {
    std::vector<int> v;
    v.push_back(1); // C++03 不支持统一初始化!
    v.push_back(2); // 只有 push_back 可用... :)
    std::for_each(v.begin(), v.end(), PrintFunc);
}

上面的代码使用 std::for_each 来迭代 vector(我们使用的是 C++98/03,所以没有基于范围的 for 循环!),然后它将 PrintFunc 作为一个可调用对象传递。

我们可以使用调用操作符将此函数转换为类类型:

// Ex1_2: 一个简单的打印功能的函数对象.
#include <algorithm>
#include <iostream>
#include <vector>

struct Printer {
    void operator()(int x) const {
        std::cout << x << '\n';
    }
};

int main() {
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2); // C++98/03 中没有初始化器列表...
    std::for_each(v.begin(), v.end(), Printer());
}

这个例子定义了一个重载了 operator() 的结构体,因此你能够像普通函数一样去“调用”它:
Printer printer;
printer(); // 调用 operator()
printer.operator()(); // 等价调用
而非成员函数通常是无状态的(你可以在常规函数中使用全局变量或静态变量,但这不是最好的解决方案,这样的方法很难跨多个 lambda 调用组控制状态),函数式的类类型却可以持有非静态成员变量从而能够保存状态。一个典型的例子是记录一个可调用对象被一个算法调用的次数。解决方案通常需要维护一个计数器,然后在每次调用时更新它的值:

// Ex1_3: 带状态的函数对象.
#include <algorithm>
#include <iostream>
#include <vector>

struct PrinterEx {
    PrinterEx(): numCalls(0) { }
    void operator()(int x) {
        std::cout << x << '\n';
        ++numCalls;
    }
    int numCalls;
};

int main() {
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    const PrinterEx vis = std::for_each(v.begin(), v.end(), PrinterEx());
    std::cout << "num calls: " << vis.numCalls << '\n';
}

在上例中,数据成员 numCalls 被用在调用运算符重载中计数此函数的调用次数。std::for_each 返回我们传入的函数对象,因此我们能够得到该对象并获取其数据成员。

如你所料,我们能够得到以下输出:
1
2
num calls: 2
我们还可以从调用作用域“捕获”变量。为此,我们必须创建一个数据成员,并在构造函数中初始化它。

// Ex1_4: 捕获变量的函数对象.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct PrinterEx {
    PrintEx(const std::string& str) :
        strText(str), numCalls(0) { }
    void operator()(int x) {
        std::cout << strText << x << '\n';
        ++numCalls;
    }
    std::string strText;
    int numCalls;
};

int main() {
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    const std::string introText("Elem: ");
    const PrinterEx vis = std::for_each(v.begin(), v.end(),
                                        PrinterEx(introText));
    std::cout << "num calls: " << vis.numCalls << '\n';
}

在这个版本中,PrinterEx 带有一个额外参数去初始化其数据成员。之后在调用运算符中使用这个变量,输出如下:
Elem: 1
Elem: 2
num calls: 2


何谓“函子”

在上面的小节中,我们有时将带有 operator() 的类类型叫做“函子”。虽然这个术语很方便,而且比“函数对象类类型”要短得多,但并不正确的。

从词源上来看,“函子”来自于函数式编程,它有不同的含义而不是 C++ 中的术语。
引用 Bartosz Milewski 中对于函子的定义:

函子是类别之间的映射。给定两类别 C 和 D,一个函子 F 能将 C 中的对象映射到 D 中的对象——它是作用在对象上的函数。

这个定义看上去相当抽象,但幸运的是,我们还可以去看到一些简化版 。在《C++函数式编程》这本书的第 10 章中,作者 Ivan Cukic 将这个抽象的定义“翻译”成更适合 C++ 语言的版本:

拥有一个定义在其上的变换(或映射)函数的类模板 F 是一个函子。

此外,这样的变换函数必须遵守恒性等和可组合性这两条规则。“函子”一词在 C++ 规范中没有以任何形式出现(即使在 C++ 98/03 中也是如此),因此在本书的其余部分,我们将尽量避免使用它。

当然,您还可以通过以下资源的阅读来了解更多关于函子的内容:


函数对象类类型的问题

如你所见,创建一个重载了调用运算符的类类型非常强大。你可以有全流程的把控,你可以以任何喜欢的方式设计它们。

然而,在 C++98/03 中,问题在于当你要用一个算法调用一个函数对象时,你却不得不在不同的地方定义它。这可能意味着可调用对象可以在源文件的前面或后面几十或几百行,甚至位于不同的翻译单元中。

作为一种可能的解决方案,您可能尝试过编写局部类,因为 C++ 支持这样的语法。但这并不适用于模板。代码如下:

// 一个局部函数对象类型
int main() {
    struct LocalPrinter {
        void operator()(int x) const {
            std::cout << x << '\n';
        }
    };
    
    std::vector<int> v(10, 1);
    std::for_each(v.begin(), v.end(), LocalPrinter());
}

尝试在 GCC 上用 -std=c++98 参数来编译它将会得到如下错误提示:
error: template argument for
'template<class _IIter, class _Funct> _Funct
std::for_each(_IIter, _IIter, _Funct)'
uses local type 'main()::LocalPrinter'
看起来,在 C++ 98/03 中,无法用局部类型实例化模板。

C++ 程序员很快就理解了这些限制,并找到了在 C++98/03 中绕过这个问题的方法。一种解决方案就是准备一组辅助类。让我们看下一节。


使用辅助类

那么,究竟什么是辅助类和和预定义的函数对象呢?

如果你去查看标准库中的 <functional> 头文件,你将会一系列可以立即用于标准库算法的类型和函数。

例如:

  • std::plus<T>() - 接受两个参数并返回它们的和。
  • std::minus<T>() - 接受两个参数并返回它们的差。
  • std::less<T>() - 接受两个参数返回是否第一个参数小于第二个参数。
  • std::greater_equal<T>() - 接受两个参数返回是否第一个参数大于等于第二个参数。
  • std::bind1st - 创建一个将第一个参数固定为所给值的可调用对象。
  • std::bind2nd - 创建一个将第二个参数固定为所给值的可调用对象。
  • std::mem_fun - 创建一个成员函数的包装对象。
  • 等等。

让我们编写一些得益于这些辅助类的代码:

// Ex1_5: 使用旧式的 C++98/03 样式的辅助类。
#include <algorithm>
#include <functional>
#include <vector>

int main() {
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    // .. push back until 9...
    const size_t smaller5 = std::count_if(v.begin(), v.end(),
                                          std::bind2nd(std::less<int>(), 5));
    return smaller5;
}

该示例使用 std::less 并通过使用 std::bind2nd 固定其第二个参数(bind1st, bind2nd 和其他函数辅助器已在 C++11 中弃用,并在 C++ 17 中移除。本章中的代码仅用于说明 C++ 98/03 中的问题。请在您的项目中使用更加现代的替代方案。)。这整个组件被传递到 count_if 中。您可能已经猜到了,代码最终转换成了一个执行简单比较的函数:
return x < 5;
如果您想要更多现成的帮助程序,那么您还可以查看 boost 库,例如boost::bind

不幸的是,这种方法的主要问题是语法复杂且难以学习。

例如,编写包含两个或多个辅助函数的代码很不自然。如下例所示:

// Ex1_6:辅助器的组合。
#include <algorithm>
#include <functional>
#include <vector>

int main() {
    using std::placeholders::_1;
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    // push_back until 9...
    const size_t val = std::count_if(v.begin(), v.end(),
                                     std::bind(std::logical_and<bool>(),
                                     std::bind(std::greater<int>(),_1, 2),
                                     std::bind(std::less_equal<int>(),_1,6)));
    return val;
}

改代码使用了 std::bind(它来自 C++ 11,所以我们作弊了,它不是 C++ 98/03)
来完成 std::greater,std::less_equal 以及 std::logical_and 的连接。此外,代码使用 _1 作为第一个输入参数的占位符。

虽然上面的代码可以工作,并且你可以在局部定义它,但是你可能已经看出来它的
复杂以及不自然的语法。且不说这个组合只代表了一个简单的条件:
return x > 2 && x <= 6;
那么,是否还有更好用更直接的方法呢?


新特性引入的动机

如你所见,在 C++98/03,调用标准库的一些算法和工具总是需要定义并传入一个可调用对象。然而,所有的可选方案都或多或少有一些限制。例如,你不能定义一个局部函数对象类型,或是使用辅助函数对象的组合,但它很复杂。

幸运的是,在 C++11 中我们终于看到了许多改进!

首先,C++ 标准委员会取消了模板实例化对局部类类型的限制。从 C++11 开始,你可以在任何你需要的局部作用域编写重载了调用操作符的类类型。

更重要的是,C++11 还带来了另一个想法:如果我们有一个简短的语法,然后编译器可以将它“展开”为相应的局部函数对象的定义呢?

这就是“lambda 表达式”的诞生!

如果我们看看 N3337—— C++11 的最终草案,我们可以看到一个关于 lambdas 的单独部分:[expr.prim.lambda]。

让我们在下一章中看看这个新特性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349