C++ 仿函数与lambda
一、使用仿函数
在 C++ 中,仿函数(Functor) 是一个类,它重载了 operator()
,使得对象能够像函数一样被调用。使用仿函数的场景通常出现在需要一种 可重用且灵活的函数对象 的地方,特别是在一些复杂的算法和数据结构中。
- 高效性:仿函数是类的对象,通常可以内联展开,而函数指针则不行,因此在一些频繁调用的场景中,仿函数效率更高。
- 状态保存:仿函数可以保存状态。例如,如果你需要一个比较操作符,它不仅可以执行比较,还可以记录操作次数,仿函数可以在类成员变量中记录状态信息,而普通函数无法做到。
- STL 算法(如
count_if
、sort
、transform
等)设计时大量依赖仿函数,通过仿函数可以使这些算法更灵活。例如,STL 的std::less
、std::greater
等常见仿函数可以作为标准的比较操作符来传递——在 STL 中,仿函数通过统一接口的设计可以方便地组合和重用,效率高、灵活性强。
何时使用仿函数:
- 复杂的逻辑或需要状态:当你需要更复杂的逻辑,或者需要在函数对象中维护某些状态时,仿函数更合适。例如,仿函数可以通过成员变量存储一些信息或进行复杂的初始化。
- 复用与扩展:如果需要在多个地方复用相同的逻辑,或者需要定义多个成员函数以支持更多功能,那么仿函数是更好的选择。
以下是一些常见的适合使用仿函数的场景:
1. 需要一个具有状态的函数时
- 仿函数可以保存状态信息,这意味着它们不仅执行操作,还可以在内部维护数据(例如计数器)。普通的函数和 lambda 表达式无法做到这一点,或者需要额外的外部数据来保存状态。
class CountCalls {
public:
int operator()() {
return ++count;
}
private:
int count = 0;
};
CountCalls countCalls;
std::cout << countCalls() << std::endl; // 输出 1
std::cout << countCalls() << std::endl; // 输出 2
这个仿函数会在每次调用时增加一个计数器,存储并管理内部的状态。
2. 将一个函数传递给算法
- 在 C++ STL 中,很多算法(例如
std::sort
、std::find_if
、std::count_if
等)需要函数或函数对象作为参数。仿函数是一种灵活的方式,它允许你在需要时携带复杂的逻辑,特别是当操作依赖于某些状态时。
class CompareGreater {
public:
bool operator()(int a, int b) const {
return a > b;
}
};
std::vector<int> vec = {1, 5, 3, 9, 2};
std::sort(vec.begin(), vec.end(), CompareGreater());
for (int num : vec) {
std::cout << num << " ";
}
// 输出: 9 5 3 2 1
3. 需要不同的操作方式
- 使用仿函数时,可以根据需要灵活定义不同的操作。比如,多个不同的仿函数可以作为比较、操作或转换的标准传递给算法。
class MultiplyBy {
public:
MultiplyBy(int factor) : factor(factor) {}
int operator()(int x) const {
return x * factor;
}
private:
int factor;
};
std::vector<int> vec = {1, 2, 3, 4, 5};
std::transform(vec.begin(), vec.end(), vec.begin(), MultiplyBy(10));
for (int num : vec) {
std::cout << num << " "; // 输出: 10 20 30 40 50
}
4. 延迟执行或动态构造函数
- 仿函数使得你能够在运行时动态地构造函数,或者在执行时使用不同的策略进行操作。比如,根据不同的条件构造不同的仿函数。
class Power {
public:
Power(int exponent) : exponent(exponent) {}
int operator()(int base) const {
return std::pow(base, exponent);
}
private:
int exponent;
};
std::vector<int> vec = {2, 3, 4};
std::transform(vec.begin(), vec.end(), vec.begin(), Power(3));
for (int num : vec) {
std::cout << num << " "; // 输出: 8 27 64
}
5. 需要一个复杂的回调机制
- 仿函数可以用作回调函数,在一些复杂的事件驱动机制中发挥作用。例如,当你需要在某些操作完成时调用某个自定义的处理逻辑时,仿函数非常有用。
class PrintMessage {
public:
PrintMessage(const std::string& msg) : message(msg) {}
void operator()() const {
std::cout << message << std::endl;
}
private:
std::string message;
};
PrintMessage print("Hello, world!");
print(); // 输出: Hello, world!
6. 需要与算法库中的函数接口兼容
- 标准库的许多算法(如
std::find_if
,std::sort
,std::accumulate
等)需要函数对象或者可调用对象作为参数。仿函数可以在这些算法中灵活应用,且其灵活性和可组合性使得它们成为一种非常常见的选择。
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << std::accumulate(vec.begin(), vec.end(), 0) << std::endl; // 求和
总结
仿函数适用于以下几种场景:
- 需要在操作中保存状态时。
- 当需要将自定义操作传递给 STL 算法时(如
sort
、count_if
等)。 - 当你需要灵活的操作方式或者根据不同条件选择不同的行为时。
- 当你希望能够像调用函数一样调用一个对象时,仿函数提供了很大的便利。
虽然 lambda 表达式 在 C++11 后成为了一个强大的替代选择,但在某些情况下,仿函数仍然有其独特的优势,特别是在需要状态保持、重用和性能优化的场合。
二、使用lambda 表达式
Lambda 表达式是 C++11 引入的一个特性,允许在函数内部定义匿名的内联函数。它通常用于需要小的、临时的函数对象(如回调函数、算法中的谓词等)时。
Lambda 表达式的语法:
[capture](parameter_list) -> return_type { body }
- capture:用于捕获外部变量。
- parameter_list:参数列表,类似于普通函数。
- return_type:返回类型,通常可以省略,编译器会推断。
- body:函数体。
Lambda 表达式可以访问和修改外部作用域中的变量, []
为空,意味着不捕获外部变量。
Lambda 表达式的简单例子:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
// 使用 Lambda 表达式打印所有元素
for_each(vec.begin(), vec.end(), [](int x) {
cout << x << " ";
});
return 0;
}
1. 排序操作
对容器中的元素进行排序并使用自定义的比较规则时,通过 Lambda 表达式直接提供比较函数,而无需单独定义一个函数。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 3};
// 使用 Lambda 表达式按升序排序
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a < b; // 返回 true 表示 a 排在 b 前面
});
// 输出排序后的结果
for (int num : nums) {
std::cout << num << " "; // 输出:1 2 3 5 8
}
return 0;
}
2. 查找满足条件的元素
通过 Lambda 表达式,可以对容器进行搜索,查找符合特定条件的元素。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {5, 8, 2, 10, 4, 7};
// 使用 Lambda 表达式查找第一个大于 5 的元素
auto it = std::find_if(nums.begin(), nums.end(), [](int x) {
return x > 5;
});
if (it != nums.end()) {
std::cout << "Found: " << *it << std::endl; // 输出:Found: 8
}
return 0;
}
3. 过滤容器元素
从容器中 移除或筛选 出不符合条件的元素。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {5, 8, 2, 10, 4, 7};
// 使用 Lambda 表达式删除小于 5 的元素
auto it = std::remove_if(nums.begin(), nums.end(), [](int x) {
return x < 5; // 删除小于 5 的元素
});
nums.erase(it, nums.end()); // 删除符合条件的元素
// 输出过滤后的容器
for (int num : nums) {
std::cout << num << " "; // 输出:8 10 7
}
return 0;
}
4. 累加器操作(如 std::accumulate
)
常用于累加、计算等聚合操作,可以直接传递给 std::accumulate
或类似的算法。
#include <iostream>
#include <vector>
#include <numeric>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// 使用 Lambda 表达式计算容器的元素和
int sum = std::accumulate(nums.begin(), nums.end(), 0, [](int a, int b) {
return a + b; // 累加两个数
});
std::cout << "Sum: " << sum << std::endl; // 输出:Sum: 15
return 0;
}
5. 在 STL 算法中作为回调函数
用作回调函数,尤其是在 STL 算法中。可以将 Lambda 作为参数传递给算法,处理复杂的业务逻辑。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {10, 20, 30, 40};
// 使用 Lambda 表达式处理每个元素(打印每个元素)
std::for_each(nums.begin(), nums.end(), [](int x) {
std::cout << x << " "; // 输出:10 20 30 40
});
return 0;
}
6. 自定义容器操作
当需要对容器进行自定义操作时,Lambda 表达式允许灵活地传递逻辑
例如自定义的转换或修改操作:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {1, 2, 3, 4};
// 使用 Lambda 表达式将每个元素乘以 2
std::for_each(nums.begin(), nums.end(), [](int &x) {
x *= 2;
});
// 输出修改后的容器
for (int num : nums) {
std::cout << num << " "; // 输出:2 4 6 8
}
return 0;
}
7. 作为函数参数
可以将 Lambda 表达式传递给其他函数作为参数,增强代码的灵活性和可读性。
#include <iostream>
#include <vector>
void applyFunction(const std::vector<int>& nums, const std::function<void(int)>& func) {
for (int num : nums) {
func(num);
}
}
int main() {
std::vector<int> nums = {1, 2, 3, 4};
// 将 Lambda 表达式作为函数参数传递
applyFunction(nums, [](int x) {
std::cout << x * x << " "; // 输出:1 4 9 16
});
return 0;
}
总结
Lambda 表达式可以简化代码、提高可读性并增强函数的表达能力。常见的使用场景包括:
- 排序、查找、过滤等 STL 算法的自定义操作。
- 作为回调函数传递给函数。
- 自定义容器元素操作,例如修改容器中的元素。
- 简化临时函数对象的编写,避免过度定义类和函数。
Lambda 的优势是尤其适合 处理简单或临时的功能需求。
三点 Lambda 表达式 相对于(优于) 仿函数 的使用场景
- 短小、临时的函数对象:Lambda 表达式通常用于那些只在函数内用到的、逻辑简单的操作。比如排序、过滤、累加等操作。
- 函数式编程:当需要将一个小的函数传递给 STL 算法时,Lambda 表达式非常合适。
- 捕获外部变量:当需要捕获并使用外部变量时,Lambda 表达式尤其有用。它可以通过捕获列表(
[ ]
)轻松访问和修改外部变量。
捕获外部变量例子:
Lambda 表达式(捕获外部变量):
int factor = 2;
std::vector<int> nums = {1, 2, 3, 4};
std::for_each(nums.begin(), nums.end(), [&factor](int &x) { x *= factor; });
仿函数(需要捕获外部变量):
struct MultiplyByFactor {
int factor;
MultiplyByFactor(int factor) : factor(factor) {}
void operator()(int &x) const { x *= factor; }
};
std::vector<int> nums = {1, 2, 3, 4};
std::for_each(nums.begin(), nums.end(), MultiplyByFactor(factor));
//for_each 是一个 STL 算法,用于对容器中的每个元素应用一个操作
//MultiplyByFactor(factor) 是创建一个 MultiplyByFactor 仿函数对象,并传入 factor
//如果 factor 是 2,那么 MultiplyByFactor(2) 创建一个倍数为 2 的仿函数对象,operator() 就会将容器中的每个元素都乘以 2。
三、仿函数 vs Lambda 表达式
在 C++11 之前,仿函数是主要的灵活选择,而在 C++11 中引入了 lambda 表达式后,很多时候可以用 lambda 表达式来代替仿函数。不过,有些场合下仿函数仍然更灵活:
- 类型安全:在一些模板设计中,仿函数的模板类类型可以进行严格的类型检查。
- 重用性:仿函数定义为一个类,可以在不同的算法中复用。lambda 表达式则更适合一次性、局部的操作。
语法区别:
- 仿函数(Function Object):需要定义一个类,并在类中重载
operator()
。 - Lambda 表达式:在代码中直接定义,可以作为匿名函数直接传递。
性能与灵活性:
-
仿函数:
- 由于是一个类,可以有成员变量、构造函数和其它成员函数,因此它可以存储状态。
- 它的灵活性更高,可以在构造函数中初始化状态或接受更多的参数。
-
Lambda 表达式:
- 通常用于没有复杂状态的临时函数,语法简洁。
- Lambda 表达式捕获外部变量,使得它可以在函数体内使用外部的局部变量。
捕获外部变量:
- Lambda 表达式 可以直接捕获外部变量(通过值或引用),这使得它在需要使用外部状态时非常方便。
- 仿函数 必须通过构造函数或其它方式传递外部参数,不能像 lambda 那样直接在函数体内捕获外部变量。
可读性与简洁性:
- Lambda 表达式 在短小的操作中更简洁,且没有额外的类定义。
- 仿函数 需要额外定义一个类,虽然它在更复杂的情境下(例如需要更多状态或成员函数时)表现出更多的灵活性。
总结
特性 | Lambda 表达式 | 仿函数 |
---|---|---|
语法 | 简洁、内联定义,不需要定义类 | 需要定义一个类并重载 operator() |
捕获外部变量 | 直接捕获外部变量 | 需要通过构造函数或其它方式传递外部参数 |
状态 | 适用于无状态或简单的临时状态 | 可以有状态(成员变量) |
灵活性 | 灵活,但不如仿函数强大 | 更强大,可以存储状态、包含多个成员函数等 |
性能 | 编译器优化好,通常没有额外的性能开销 | 可能稍微复杂一些,尤其在需要传递状态时 |
可读性 | 更简洁,适合短小操作 | 更冗长,但适用于复杂操作或需要更多灵活性时 |
- Lambda 表达式 适合短小、临时的函数对象,特别是当你只需要传递一个简单的比较或操作时。
- 仿函数 更适合需要状态、复杂逻辑或者需要复用的情况。