目录
(图像由AI生成)
0.前言
随着C++11标准的引入,C++语言发生了翻天覆地的变化,其中包括了列表初始化、右值引用、智能指针、并发支持等众多特性。在这些新特性中,lambda表达式和包装器是两个非常值得关注的工具。它们不仅增强了语言的表达能力,还使代码更简洁和更具可读性。本文将详细探讨C++11中的lambda表达式和包装器。
1.lambda表达式
1.1C++98中的例子
在C++98中,进行自定义排序通常需要借助于函数指针或函数对象。下面的例子展示了如何在C++98中使用std::sort
对数组进行排序。
在不需要自定义排序规则时,可以简单地使用std::sort
默认的升序排列:
#include <algorithm>
#include <iostream>
int main() {
int array[] = {4, 1, 8, 5, 3, 7, 0, 9, 2, 6};
// 默认升序排序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
std::cout << array[i] << " ";
}
std::cout << std::endl;
return 0;
}
在需要降序排序时,我们可以通过传递自定义的比较函数来改变排序规则:
#include <algorithm>
#include <iostream>
#include <functional>
int main() {
int array[] = {4, 1, 8, 5, 3, 7, 0, 9, 2, 6};
// 自定义降序排序
std::sort(array, array + sizeof(array) / sizeof(array[0]), std::greater<int>());
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
std::cout << array[i] << " ";
}
std::cout << std::endl;
return 0;
}
在上述例子中,我们使用了标准库中的std::greater<int>()
作为自定义比较函数,以实现降序排序。如果我们需要对自定义类型的对象进行排序,那么就必须定义一个比较规则。以下是如何通过创建一个函数对象来完成这一操作的示例:
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
struct Goods {
std::string _name; // 商品名称
double _price; // 商品价格
int _evaluate; // 商品评价
Goods(const std::string& name, double price, int evaluate)
: _name(name), _price(price), _evaluate(evaluate) {}
};
// 比较价格(升序)的函数对象
struct ComparePriceLess {
bool operator()(const Goods& g1, const Goods& g2) const {
return g1._price < g2._price;
}
};
// 比较价格(降序)的函数对象
struct ComparePriceGreater {
bool operator()(const Goods& g1, const Goods& g2) const {
return g1._price > g2._price;
}
};
int main() {
std::vector<Goods> goodsList = {
{"苹果", 2.1, 5},
{"香蕉", 3.0, 4},
{"橘子", 2.2, 3},
{"葡萄", 1.5, 4}
};
// 使用自定义函数对象进行升序排序
std::sort(goodsList.begin(), goodsList.end(), ComparePriceLess());
std::cout << "按价格升序排序结果:" << std::endl;
for (const auto& goods : goodsList) {
std::cout << goods._name << " " << goods._price << " " << goods._evaluate << std::endl;
}
// 使用自定义函数对象进行降序排序
std::sort(goodsList.begin(), goodsList.end(), ComparePriceGreater());
std::cout << "\n按价格降序排序结果:" << std::endl;
for (const auto& goods : goodsList) {
std::cout << goods._name << " " << goods._price << " " << goods._evaluate << std::endl;
}
return 0;
}
//输出结果:
按价格升序排序结果:
葡萄 1.5 4
苹果 2.1 5
橘子 2.2 3
香蕉 3 4按价格降序排序结果:
香蕉 3 4
橘子 2.2 3
苹果 2.1 5
葡萄 1.5 4
在这个示例中,我们定义了一个名为Goods
的结构体表示商品,并定义了两个函数对象ComparePriceLess
和ComparePriceGreater
,用于比较商品的价格。我们通过这些函数对象实现了商品的升序和降序排序。
在C++98中,处理自定义排序通常需要定义额外的比较函数或函数对象。这种方式在复杂情况下显得冗长且不够灵活。为了解决这些问题,C++11引入了lambda表达式,使得代码的编写更加简洁和灵活。接下来我们将讨论C++11中的lambda表达式,看看它如何简化代码和提高可读性。
1.2lambda表达式语法
C++11中的lambda表达式引入了一种便捷方式来定义匿名函数。它允许在需要函数对象的任何地方编写内联的函数逻辑,简化代码结构,提升可读性和可维护性。
基本语法
[capture-list](parameters) mutable -> return-type {
// function body
}
- [capture-list](捕获列表):用于指定lambda表达式能够使用的外部变量。它规定了lambda可以捕获哪些变量,以及捕获方式(按值或按引用)。
- (parameters)(参数列表):类似于普通函数的参数列表,用于传递参数。
- mutable:可选项,允许lambda在捕获的变量上进行修改。
- -> return-type(返回类型):指定lambda的返回类型,如果可以推断,则可省略。
- {statement}(函数体):函数逻辑的具体实现部分。
详细说明
-
捕获列表(capture-list)
捕获列表允许lambda函数访问其外部作用域中的变量,捕获方式可以按值或按引用。
- 按值捕获:在捕获列表中使用
=
或具体变量名,如[=]
或[x]
。这会复制捕获的变量,lambda内部不能修改原变量。 - 按引用捕获:在捕获列表中使用
&
或具体变量名前加&
,如[&]
或[&x]
。这允许lambda修改原变量。
示例:
int a = 5, b = 10; // 按值捕获所有变量 auto captureAllByValue = [=]() { return a + b; }; // 按引用捕获所有变量 auto captureAllByReference = [&]() { a += b; }; // 混合捕获:按值捕获a,按引用捕获b auto mixedCapture = [a, &b]() { b += a; };
- 按值捕获:在捕获列表中使用
-
参数列表(parameters)
参数列表与普通函数的参数列表相同,如果不需要传递参数可以省略。
示例:
// 带参数的lambda auto add = [](int x, int y) { return x + y; };
-
mutable关键字
默认情况下,lambda表达式是不可修改捕获的值的,如果需要修改捕获的值,需使用
mutable
关键字。示例:
int x = 10; auto mutableLambda = [x]() mutable { x *= 2; // 修改了捕获的值x return x; };
-
返回类型(return-type)
如果编译器能够推断返回类型,则可以省略返回类型。显式指定返回类型在需要时可以提高代码的明确性。
示例:
// 显式指定返回类型为int auto multiply = [](int a, int b) -> int { return a * b; };
-
函数体(statement)
函数体是lambda逻辑的核心部分,包含实际执行的代码。
示例:
// 函数体中执行简单的打印操作 auto greet = []() { std::cout << "Hello, Lambda!" << std::endl; };
捕获列表注意事项
-
父作用域指向包含lambda函数的语句块。
-
捕获列表可以包含多个捕获项,并以逗号分隔。
-
捕获列表不允许变量重复传递,否则编译时将报错。
int x = 5; auto lambdaError = [=, &x]() { // 错误:x已经通过按值捕获 };
-
在块作用域以外的lambda函数,捕获列表必须为空。
-
在块作用域中的lambda函数,仅能捕获父作用域中声明的变量,捕获任何非局部变量会导致错误。
-
lambda表达式之间不能相互赋值,即使看起来类型相同。
示例代码
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int a = 3, b = 4;
// 基本的lambda表达式,没有捕获变量
auto simpleLambda = []() { std::cout << "Hello, Lambda!" << std::endl; };
simpleLambda();
// 使用捕获列表的lambda表达式
auto captureExample = [a, &b](int c) -> int {
b += a + c;
return b;
};
std::cout << "Capture Example: " << captureExample(5) << std::endl;
// 使用mutable关键字
auto mutableExample = [a]() mutable {
a *= 2;
return a;
};
std::cout << "Mutable Example: " << mutableExample() << std::endl;
// lambda用于排序
std::vector<int> numbers = {4, 2, 8, 1, 7};
std::sort(numbers.begin(), numbers.end(), [](int x, int y) {
return x < y;
});
std::cout << "Sorted numbers: ";
for (const auto &num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
//输出结果:
Hello, Lambda!
Capture Example: 12
Mutable Example: 6
Sorted numbers: 1 2 4 7 8
1.3函数对象与lambda表达式
在 C++ 中,函数对象(也称为仿函数)是通过重载 operator()
的类的实例,使得对象可以像函数一样被调用。C++11 引入的 lambda 表达式在许多方面可以看作是对函数对象的一种语法糖,两者都用于定义可以被调用的对象。
函数对象通过重载 operator()
使对象行为类似于函数。以下是一个示例:
#include <iostream>
// 定义一个函数对象类
class Rate {
public:
Rate(double rate) : _rate(rate) {}
// 重载operator()实现函数调用
double operator()(double money, int year) const {
return money * _rate * year;
}
private:
double _rate; // 利率
};
int main() {
double rate = 0.49;
Rate r1(rate); // 创建函数对象实例
double result = r1(10000, 2); // 使用函数对象进行计算
std::cout << "Result using function object: " << result << std::endl;
return 0;
}
说明:
- 构造函数:函数对象通常通过构造函数接收参数,以初始化对象的内部状态。
- 重载
operator()
:通过重载operator()
,函数对象可以像普通函数一样被调用。 - 状态维护:函数对象可以维护状态,这使得它们在复杂情况下非常有用。
lambda 表达式在很多场景中提供了比函数对象更简洁的语法,尤其是在需要定义简单逻辑的情况下。lambda 表达式的语法允许在代码的上下文中直接定义匿名函数。
#include <iostream>
int main() {
double rate = 0.49;
// 使用lambda表达式定义函数
auto r2 = [rate](double money, int year) -> double {
return money * rate * year;
};
double result = r2(10000, 2); // 调用lambda表达式
std::cout << "Result using lambda expression: " << result << std::endl;
return 0;
}
说明:
- 捕获列表:lambda 表达式使用捕获列表来捕获外部变量,这与函数对象使用成员变量维护状态的方式类似。
- 简洁性:lambda 表达式提供了一种更为简洁的语法来实现与函数对象类似的功能,特别是在需要快速定义简单逻辑的情况下。
- 灵活性:通过捕获列表,lambda 表达式可以灵活地选择按值或按引用捕获变量,类似于传递参数给函数对象。
底层实现对比
实际上,在编译时,编译器会为每个 lambda 表达式生成一个匿名的类,该类实现了 operator()
。从使用方式上看,lambda 表达式和函数对象是完全等价的。在底层,编译器对它们的处理方式相同。
// 函数对象的使用
Rate r1(rate);
r1(10000, 2);
// lambda表达式的使用
auto r2 = [rate](double money, int year) -> double { return money * rate * year; };
r2(10000, 2);
在底层实现中,编译器会自动为 lambda 表达式创建一个类似于 Rate
类的匿名类,并实现 operator()
。这意味着 lambda 表达式在功能上与函数对象相似,二者可以互换使用。
2.包装器
2.1包装器简介
在C++11中,包装器(Wrapper)提供了一种更灵活的方式来管理和调用可调用对象。包装器的核心思想是通过抽象层对各种函数对象、函数指针、lambda表达式等进行统一处理,从而提高代码的灵活性和可扩展性。
为什么需要包装器?
在C++编程中,我们经常需要处理不同类型的可调用对象,如普通函数、成员函数、函数对象和lambda表达式。包装器的引入是为了提供一种统一的接口来处理这些不同的可调用对象,使得我们可以以一致的方式存储和调用它们。
包装器的优势
-
统一接口: 包装器提供了统一的接口来处理各种可调用对象,避免了为每种类型编写单独的调用逻辑。
-
类型安全: 包装器通过模板实现,可以确保调用时的参数和返回类型的类型安全。
-
灵活性: 包装器支持对可调用对象的动态绑定,使得我们可以在运行时决定具体的调用逻辑。
常用的包装器
-
std::function
:一个通用的、多态的函数包装器,能够存储任何类型的可调用对象,包括函数指针、函数对象和lambda表达式。 -
std::bind
:用于创建函数对象或函数指针的绑定器,能够绑定参数以创建一个新的可调用对象,适用于部分应用场景。
2.2function包装器
std::function
是 C++11 中引入的一个通用、多态的函数包装器。它能够存储、复制和调用任何可调用目标——如普通函数、函数对象、lambda 表达式和指向成员函数的指针等。std::function
提供了一个统一的接口,使得我们可以灵活地使用不同类型的可调用对象。
为什么需要 std::function
?
在 C++ 中,存在多种可调用对象:普通函数、函数对象、函数指针和 lambda 表达式等。这些不同类型的可调用对象在使用时需要不同的语法和处理方式。如果我们想要在一个数据结构中存储和调用这些不同类型的可调用对象,传统的方法可能会导致代码复杂且难以维护。std::function
的出现正是为了解决这个问题,通过提供一个统一的接口来管理这些不同的可调用对象。
std::function
的定义
在头文件 <functional>
中,std::function
被定义为一个模板类,允许指定返回类型和参数类型。
#include <functional>
// 类模板定义
template <class Ret, class... Args>
class function<Ret(Args...)>;
Ret
:被调用函数的返回类型。Args...
:被调用函数的参数类型。
使用 std::function
我们可以使用 std::function
来存储不同类型的可调用对象。以下是一些常见的使用场景:
- 存储普通函数
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
// 使用std::function存储普通函数
std::function<int(int, int)> func1 = add;
std::cout << "Function pointer: " << func1(2, 3) << std::endl; // 输出:5
return 0;
}
- 存储函数对象
#include <iostream>
#include <functional>
struct Multiply {
int operator()(int a, int b) {
return a * b;
}
};
int main() {
// 使用std::function存储函数对象
std::function<int(int, int)> func2 = Multiply();
std::cout << "Function object: " << func2(2, 3) << std::endl; // 输出:6
return 0;
}
- 存储 lambda 表达式
#include <iostream>
#include <functional>
int main() {
// 使用std::function存储lambda表达式
std::function<int(int, int)> func3 = [](int a, int b) { return a - b; };
std::cout << "Lambda expression: " << func3(5, 3) << std::endl; // 输出:2
return 0;
}
- 存储成员函数指针
#include <iostream>
#include <functional>
class Calculator {
public:
int divide(int a, int b) {
return a / b;
}
};
int main() {
Calculator calc;
// 使用std::function存储成员函数指针
std::function<int(Calculator&, int, int)> func4 = &Calculator::divide;
std::cout << "Member function pointer: " << func4(calc, 10, 2) << std::endl; // 输出:5
return 0;
}
以下示例展示了 std::function
如何在一个模板函数中被灵活使用:
#include <iostream>
#include <functional>
// 模板函数,接受任意可调用对象
template <class F, class T>
T useF(F f, T x) {
static int count = 0;
std::cout << "count: " << ++count << std::endl; // 调用次数
return f(x); // 调用传入的可调用对象
}
// 普通函数
double f(double i) {
return i / 2;
}
// 函数对象
struct Functor {
double operator()(double d) {
return d / 3;
}
};
int main() {
// 使用std::function包装普通函数
std::function<double(double)> func1 = f;
std::cout << "useF with function: " << useF(func1, 11.11) << std::endl;
// 使用std::function包装函数对象
std::function<double(double)> func2 = Functor();
std::cout << "useF with functor: " << useF(func2, 11.11) << std::endl;
// 使用std::function包装lambda表达式
std::function<double(double)> func3 = [](double d) -> double { return d / 4; };
std::cout << "useF with lambda: " << useF(func3, 11.11) << std::endl;
return 0;
}
在上述代码中,我们定义了一个模板函数 useF
,它接收任何类型的可调用对象,并输出调用次数。这展示了 std::function
的灵活性和可扩展性。
2.3bind包装器
std::bind
是 C++11 中引入的一个强大的工具,它用于创建函数适配器,允许我们将参数绑定到函数或对象的成员函数上,从而生成新的可调用对象。通过 std::bind
,我们可以调整函数调用的参数顺序、绑定固定参数或部分应用参数。这种灵活性使得 std::bind
成为 C++ 中函数编程的重要工具之一。
std::bind
的定义与用途
std::bind
定义在 <functional>
头文件中,实质上是一个函数模板,接受一个可调用对象(如函数、函数指针、函数对象或成员函数指针)及其参数,并返回一个新的可调用对象。
模板原型
#include <functional>
template <class Fn, class... Args>
unspecified bind(Fn&& fn, Args&&... args);
Fn
:可调用对象的类型。Args...
:函数参数的类型。
调用 std::bind
时,返回一个新的可调用对象,该对象可以存储并调用绑定的函数以及参数。
使用 std::bind
的一般形式:
auto newCallable = std::bind(callable, arg_list);
newCallable
:返回的新可调用对象。callable
:要绑定的可调用对象。arg_list
:指定参数列表,可以包含占位符。
当调用 newCallable
时,newCallable
会调用 callable
并传递 arg_list
中的参数。
以下示例展示了如何使用 std::bind
来绑定函数和参数:
#include <iostream>
#include <functional>
// 普通函数
int Plus(int a, int b) {
return a + b;
}
// 类的成员函数
class Sub {
public:
int sub(int a, int b) {
return a - b;
}
};
int main() {
// 使用std::bind绑定普通函数
// std::placeholders::_1 和 std::placeholders::_2 是占位符,用于绑定的参数
std::function<int(int, int)> func1 = std::bind(Plus, std::placeholders::_1, std::placeholders::_2);
std::cout << "Result of func1: " << func1(3, 4) << std::endl; // 输出:7
// 绑定成员函数
Sub s;
std::function<int(int, int)> func2 = std::bind(&Sub::sub, s, std::placeholders::_1, std::placeholders::_2);
std::cout << "Result of func2: " << func2(10, 5) << std::endl; // 输出:5
// 绑定参数顺序交换
std::function<int(int, int)> func3 = std::bind(Plus, std::placeholders::_2, std::placeholders::_1);
std::cout << "Result of func3: " << func3(3, 4) << std::endl; // 输出:7
// 绑定固定参数
std::function<int(int)> func4 = std::bind(Plus, 10, std::placeholders::_1);
std::cout << "Result of func4: " << func4(5) << std::endl; // 输出:15
return 0;
}
std::bind
用法解析
-
占位符:
std::placeholders::_1
、std::placeholders::_2
等用于指定函数调用时参数的位置。例如,std::placeholders::_1
表示函数调用的第一个参数。 -
绑定顺序:我们可以通过调整占位符的顺序来更改参数的调用顺序。在
func3
中,参数顺序被交换,实现了灵活的调用。 -
绑定固定参数:在
func4
中,我们将第一个参数固定为10
,即每次调用func4
时,Plus
的第一个参数始终为10
。 -
成员函数绑定:绑定成员函数时,需要使用
&ClassName::MemberFunction
的形式,并传递一个类实例。
std::bind
的优势
- 简化代码:通过
std::bind
,我们可以简化函数调用和参数绑定的过程,减少重复代码。 - 增强灵活性:
std::bind
提供了对参数顺序和绑定的灵活控制,使得函数调用更具可配置性。 - 提高可读性:通过显式地绑定参数和使用占位符,代码逻辑更加清晰。
3.结语
C++11的lambda表达式和包装器为程序员提供了更为强大和灵活的工具,使得C++代码更加简洁、可读性更高。lambda表达式通过简化函数对象的使用过程,提高了代码的表达能力;而包装器std::function
和std::bind
则提供了处理可调用对象的统一接口和灵活性。理解并掌握这些特性,不仅能提高代码的质量,也能极大地增强开发者的生产力。随着对C++11特性的深入了解,程序员将能更好地发挥这门语言的强大能力。