一、函数对象
定义:任何定义了函数调用操作符的对象都是函数对象。
函数:
- 函数可以使用()进行调用
- 函数在参数传递时通常退化为函数指针
函数指针
- 函数指针可以解引用调用:(*ptr)(...)参数列表
- 函数指针也可以直接调用:ptr(...)参数列表
定义了operator()成员函数的对象
- 语法和普通函数一致:obj(...);
函数对象是指重载了 operator() 的类实例,可以像函数一样被调用。下面是一个简单的函数对象实现示例:
#include <iostream>
// 简单的函数对象类
class Adder {
private:
int base; // 基数值
public:
// 构造函数
Adder(int b) : base(b) {}
// 重载 operator()
int operator()(int x) const {
return base + x;
}
};
int main() {
// 创建函数对象实例,base 设为 5
Adder add5(5);
// 像函数一样调用
std::cout << "5 + 3 = " << add5(3) << std::endl; // 输出: 5 + 3 = 8
std::cout << "5 + 7 = " << add5(7) << std::endl; // 输出: 5 + 7 = 12
// 也可以创建另一个基数的加法器
Adder add10(10);
std::cout << "10 + 4 = " << add10(4) << std::endl; // 输出: 10 + 4 = 14
return 0;
}
更通用的函数对象模板
如果需要更通用的实现,可以使用模板:
#include <iostream>
template <typename T>
class Multiplier {
private:
T factor;
public:
Multiplier(T f) : factor(f) {}
T operator()(T x) const {
return factor * x;
}
};
int main() {
Multiplier<int> times2(2);
std::cout << "2 * 3 = " << times2(3) << std::endl; // 输出: 2 * 3 = 6
Multiplier<double> times0_5(0.5);
std::cout << "0.5 * 7 = " << times0_5(7.0) << std::endl; // 输出: 0.5 * 7 = 3.5
return 0;
}
带状态的函数对象
函数对象可以保持状态,这是普通函数无法做到的:
#include <iostream>
class Counter {
private:
int count;
public:
Counter() : count(0) {}
int operator()() {
return ++count;
}
void reset() {
count = 0;
}
};
int main() {
Counter counter;
std::cout << counter() << std::endl; // 输出: 1
std::cout << counter() << std::endl; // 输出: 2
std::cout << counter() << std::endl; // 输出: 3
counter.reset();
std::cout << counter() << std::endl; // 输出: 1
return 0;
}
二、lambda表达式
Lambda 表达式是 C++11 引入的一种简洁的匿名函数定义方式,可以用于创建函数对象。Lambda 表达式通常用于需要传递简单函数作为参数的场合,比如 STL 算法中的谓词或比较函数。
下面是一些简单的 lambda 表达式示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 示例 1: 简单的 lambda 表达式
auto greet = []() {
std::cout << "Hello, World!" << std::endl;
};
greet(); // 输出: Hello, World!
// 示例 2: 带参数的 lambda 表达式
auto add = [](int a, int b) {
return a + b;
};
std::cout << "3 + 4 = " << add(3, 4) << std::endl; // 输出: 3 + 4 = 7
// 示例 3: 捕获外部变量的 lambda 表达式
int x = 10;
auto multiply_by_x = [x](int y) {
return x * y;
};
std::cout << "10 * 5 = " << multiply_by_x(5) << std::endl; // 输出: 10 * 5 = 50
// 示例 4: 在 STL 算法中使用 lambda 表达式
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用 lambda 表达式计算平方
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n * n << " ";
});
std::cout << std::endl; // 输出: 1 4 9 16 25 36 49 64 81 100
// 示例 5: 捕获列表中使用引用
int sum = 0;
std::for_each(numbers.begin(), numbers.end(), [&sum](int n) {
sum += n;
});
std::cout << "Sum of numbers: " << sum << std::endl; // 输出: Sum of numbers: 55
// 示例 6: 捕获列表中使用 mutable
int count = 0;
auto increment = [&count]() mutable {
count++;
};
increment();
increment();
std::cout << "Count: " << count << std::endl; // 输出: Count: 2
return 0;
}
解释
-
基本语法:Lambda 表达式的基本语法是
[capture list](parameters) -> return type { body }。 -
捕获列表:用于捕获外部作用域中的变量。可以是值捕获
[x]或引用捕获[&x],也可以使用[=]捕获所有变量为值,[&]捕获所有变量为引用。 -
参数列表:类似于普通函数的参数列表。
-
返回类型:可以显式指定返回类型,也可以自动推导。
-
函数体:定义 lambda 表达式的行为。
-
mutable 关键字:允许在 lambda 表达式中修改值捕获的变量。
-
消除多重初始化路径
-
参数列表中[*this],是按值捕获自己对象,会创建一个新的对象;[this]是按引用捕获自己对象
Lambda 表达式在需要传递简单函数逻辑时非常有用,尤其是在 STL 算法中,它们可以使代码更简洁和易读。
三、泛型lambda表达式
泛型 lambda 表达式是 C++14 引入的一个特性,允许 lambda 表达式使用自动类型推导的参数类型,这使得 lambda 表达式更加灵活和通用。通过使用 auto 关键字,泛型 lambda 表达式可以处理多种类型的参数。
下面是一些泛型 lambda 表达式的示例:
示例 1: 简单的泛型 lambda 表达式
#include <iostream>
int main() {
// 定义一个泛型 lambda 表达式,接受两个参数并返回它们的和
auto add = [](auto a, auto b) {
return a + b;
};
// 测试不同类型的加法
std::cout << "3 + 4 = " << add(3, 4) << std::endl; // 输出: 3 + 4 = 7
std::cout << "3.5 + 4.5 = " << add(3.5, 4.5) << std::endl; // 输出: 3.5 + 4.5 = 8
std::cout << "'a' + 'b' = " << add('a', 'b') << std::endl; // 输出: 'a' + 'b' = 195
return 0;
}
示例 2: 泛型 lambda 表达式用于排序
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9};
// 使用泛型 lambda 表达式进行排序
std::sort(numbers.begin(), numbers.end(), [](auto a, auto b) {
return a < b;
});
// 输出排序后的结果
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 1 2 5 8 9
return 0;
}
示例 3: 泛型 lambda 表达式用于打印任意类型的值
#include <iostream>
#include <vector>
#include <string>
int main() {
// 定义一个泛型 lambda 表达式,用于打印任意类型的值
auto print = [](const auto& value) {
std::cout << value << std::endl;
};
// 测试不同类型的打印
print(42); // 输出: 42
print(3.14); // 输出: 3.14
print("Hello, World!"); // 输出: Hello, World!
std::vector<int> numbers = {1, 2, 3};
print(numbers); // 输出: 0x... (地址)
return 0;
}
示例 4: 泛型 lambda 表达式用于处理自定义类型
#include <iostream>
struct Point {
int x;
int y;
};
int main() {
// 定义一个泛型 lambda 表达式,用于打印任意类型的成员
auto printMember = [](const auto& obj, int (Point::*member)) {
std::cout << obj.*member << std::endl;
};
Point p = {10, 20};
// 打印 Point 对象的成员
printMember(p, &Point::x); // 输出: 10
printMember(p, &Point::y); // 输出: 20
return 0;
}
总结
泛型 lambda 表达式通过使用 auto 关键字,使得 lambda 表达式可以接受和操作多种类型的参数。这种特性在需要处理多种类型数据时非常有用,可以增加代码的灵活性和可重用性。通过泛型 lambda 表达式,可以简化代码并减少重复的类型定义。
四、std::function
- 不同类型的函数对象可以抹去所有差异,原型相同就可以放大function对象中
- function对象创建和销毁可能使用堆上内存;执行则相当于普通虚函数调用
std::transform 是 C++ 标准库 <algorithm> 头文件中的一个算法函数,用于对容器(如 vector、list、string 等)中的元素进行转换操作,并将结果存储到另一个容器中。以下是对 std::transform 的详细介绍:
函数原型
std::transform 有两种重载形式:
- 单输入范围转换:
template <class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op);
first1、last1:定义输入范围的迭代器。
d_first:输出范围的起始迭代器。
unary_op:对单个元素应用的一元操作函数或函数对象。
- 2.双输入范围转换:
template <class InputIt1, class InputIt2, class OutputIt, class BinaryOperation>
OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOperation binary_op);
first1、last1:定义第一个输入范围的迭代器。
first2:定义第二个输入范围的起始迭代器。
d_first:输出范围的起始迭代器。
binary_op:对两个元素应用的二元操作函数或函数对象。
使用示例
-
单输入范围转换示例
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squared_numbers(numbers.size());
// 计算每个元素的平方
std::transform(numbers.begin(), numbers.end(), squared_numbers.begin(),
[](int x) { return x * x; });
// 输出结果
for (int num : squared_numbers) {
std::cout << num << " ";
}
// 输出: 1 4 9 16 25
return 0;
}
双输入范围转换示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers1 = {1, 2, 3, 4, 5};
std::vector<int> numbers2 = {10, 20, 30, 40, 50};
std::vector<int> results(numbers1.size());
// 计算两个对应元素的和
std::transform(numbers1.begin(), numbers1.end(), numbers2.begin(), results.begin(),
std::plus<int>());
// 输出结果
for (int num : results) {
std::cout << num << " ";
}
// 输出: 11 22 33 44 55
return 0;
}
注意事项
- 迭代器有效性:确保提供的迭代器是有效的,并且指向的元素数量匹配。
- 输出容器大小:在调用
std::transform之前,确保输出容器有足够的空间来存储结果。可以使用resize方法预先分配空间,或者使用back_inserter来动态添加元素。 - Lambda 表达式:可以使用 lambda 表达式作为操作函数,以增加灵活性。
- 性能:
std::transform通常比手动循环更高效,因为编译器可能对其进行优化。
应用场景
- 数值转换:如计算平方、加倍等。
- 字符串处理:如转换大小写。
- 数据合并:如计算两个向量的对应元素之和。
- 类型转换:如将字符串转换为长度。
五、将若干个即将立即执行的 lambda 表达式放到 std::function 对象中,在某些场合下可能不太适合,尤其是当需要使用 function 模板进行类型擦除或存储时。这种做法可能带来以下问题:
1. 性能开销
- 类型擦除成本:
std::function是一个通用的函数包装器,它通过类型擦除(type erasure)来存储任意可调用对象。这种机制通常涉及动态内存分配和间接调用(通过虚函数或类似机制),导致性能开销。 - 立即执行:如果 lambda 表达式是立即执行的,并且结果不需要被存储或复用,那么将其包装在
std::function中是多余的,因为直接执行 lambda 表达式会更高效。
2. 代码复杂性和可读性
- 不必要的间接性:将立即执行的 lambda 表达式放入
std::function中,会使代码逻辑变得复杂和难以理解。读者可能会困惑为什么需要额外的包装层。 - 冗余代码:如果 lambda 表达式只是为了立即执行某个操作,并且结果不需要进一步使用,那么直接执行它会更简洁明了。
3. 类型安全和灵活性
- 类型不匹配:
std::function需要指定签名(返回类型和参数类型)。如果 lambda 表达式的签名与std::function的模板参数不匹配,将导致编译错误。对于立即执行的 lambda 表达式,通常不需要这种类型约束。 - 灵活性降低:
std::function通常用于需要存储和延迟执行可调用对象的场景。如果 lambda 表达式是立即执行的,那么使用std::function反而限制了灵活性,因为无法利用编译时多态性。
4. 使用场景不匹配
- 不适合立即执行:
std::function更适合用于需要存储可调用对象并在稍后执行的场景,例如回调机制、事件处理等。对于立即执行的 lambda 表达式,这种存储和延迟执行的能力是多余的。 - 替代方案:如果需要在某个作用域内执行多个操作,并且这些操作之间没有共享状态或延迟执行的需求,那么直接顺序执行这些 lambda 表达式会更合适。
示例说明
假设有以下场景,需要将多个立即执行的 lambda 表达式放入 std::function 中:
#include <iostream>
#include <functional>
int main() {
// 不推荐的做法:将立即执行的 lambda 表达式放入 std::function 中
std::function<void()> func1 = []() { std::cout << "Lambda 1 executed" << std::endl; };
std::function<void()> func2 = []() { std::cout << "Lambda 2 executed" << std::endl; };
func1(); // 立即执行
func2(); // 立即执行
// 更简单和高效的做法:直接执行 lambda 表达式
[]() { std::cout << "Direct Lambda 1 executed" << std::endl; }();
[]() { std::cout << "Direct Lambda 2 executed" << std::endl; }();
return 0;
}
在这个例子中,直接执行 lambda 表达式比将它们包装在 std::function 中更简单、更高效。
结论
将若干个即将立即执行的 lambda 表达式放入 std::function 对象中,在大多数情况下是不合适的,因为:
- 它引入了不必要的性能开销。
- 它增加了代码的复杂性和降低了可读性。
- 它限制了类型安全和灵活性。
- 它与
std::function的典型使用场景不匹配。
对于立即执行的 lambda 表达式,直接执行它们是更简单、更高效的选择。
六、推荐无差别地使用 [=](按值捕获所有外部变量)
在产品代码中,不推荐无差别地使用 [=](按值捕获所有外部变量)作为 lambda 表达式的默认捕获方式,主要原因涉及代码的可维护性、可读性、潜在的性能开销以及隐藏的逻辑错误风险。以下是详细的分析:
1. 降低代码的可读性和可维护性
- 隐式依赖:
[=]会捕获 lambda 表达式所在作用域内的所有外部变量,但不会显式列出这些变量。这使得代码的读者难以快速了解 lambda 表达式具体依赖哪些外部变量。 - 难以调试:当 lambda 表达式出现意外行为时,由于捕获的变量不透明,调试和排查问题会变得更加困难。
示例对比:
// 使用 [=] 捕获所有变量(不推荐)
auto func1 = [=]() {
std::cout << x << ", " << y << std::endl;
};
// 显式捕获需要的变量(推荐)
auto func2 = [x, y]() {
std::cout << x << ", " << y << std::endl;
};
在 func2 中,通过显式列出 x 和 y,代码的意图更加清晰。
2. 增加不必要的性能开销
- 不必要的拷贝:
[=]会对所有外部变量进行值拷贝,即使某些变量在 lambda 表达式中并未使用。这会导致不必要的内存分配和拷贝操作,影响性能。 - 大对象的拷贝问题:如果捕获的变量是大型对象(如
std::vector或自定义类),按值拷贝会显著增加开销。
示例:
std::vector<int> largeData(1000000, 42);
// 不推荐:largeData 被按值拷贝,即使未使用
auto badLambda = [=]() {
std::cout << "Hello" << std::endl;
};
// 推荐:按引用捕获或避免捕获
auto goodLambda = []() {
std::cout << "Hello" << std::endl;
};
3. 可能导致悬垂引用或生命周期问题
- 隐式按值捕获的潜在问题:虽然
[=]是按值捕获,但如果 lambda 表达式被存储并在原变量生命周期结束后执行,可能引发未定义行为(尽管按值捕获本身不会导致悬垂引用,但混合使用[=]和[&]或捕获局部变量可能导致混淆)。 - 更危险的是
[&]:如果误用[&](按引用捕获所有变量),而 lambda 被延迟执行,则极易导致悬垂引用。虽然[=]本身不会,但混合使用会增加风险。
示例(潜在混淆):
int x = 10;
auto lambda = [=, &x]() { // 混合使用 [=] 和 [&]
x = 20; // 修改的是外部的 x(引用捕获)
std::cout << x << std::endl;
};
lambda(); // 输出 20
// 如果 lambda 被存储并在 x 生命周期结束后执行,则 [=] 捕获的其他变量可能安全,但这种混合使用容易出错
4. 难以追踪变量的修改
- 修改的隐蔽性:如果 lambda 表达式修改了捕获的变量(通过引用捕获或某些隐式行为),使用
[=]会让代码的读者难以发现这些修改,从而引发难以调试的错误。 - 一致性缺失:团队成员可能对
[=]的行为理解不一致,导致代码风格不统一。
示例:
int value = 10;
auto func = [=]() mutable { // 需要 mutable 才能修改值捕获的变量
value = 20; // 修改的是 lambda 内部的拷贝
};
func();
std::cout << value << std::endl; // 输出 10(外部变量未被修改)
// 这种行为可能不符合预期,且难以通过代码直接发现
5. 推荐的最佳实践
- 显式捕获:总是显式列出 lambda 表达式需要捕获的变量,避免使用
[=]或[&]作为默认捕获方式。- 按值捕获:
[x, y] - 按引用捕获:
[&x, &y] - 混合捕获:根据需要组合使用,如
[x, &y]
- 按值捕获:
- 避免捕获不必要的变量:只捕获 lambda 表达式真正需要的变量。
- 使用
mutable谨慎:如果需要修改按值捕获的变量,明确使用mutable关键字,并确保这种修改是合理的。
总结
在产品代码中,不推荐使用 [=] 的主要原因是:
- 降低可读性和可维护性:隐式捕获所有变量,代码意图不清晰。
- 增加性能开销:不必要的拷贝,尤其是对于大对象。
- 潜在的生命周期和逻辑错误:混合使用捕获方式或对捕获行为理解不一致可能导致错误。
- 难以追踪修改:修改的隐蔽性增加了调试难度。
推荐做法:始终显式列出需要捕获的变量,确保代码清晰、高效且易于维护。

590

被折叠的 条评论
为什么被折叠?



