跳到主要内容

C++ 函数对象最佳实践

函数对象是什么

函数对象(Function Object),也称为仿函数(Functor),是C++中一种特殊的对象,它可以像函数一样被调用。本质上,函数对象是一个重载了函数调用运算符operator()的类或结构体的实例。

提示

函数对象结合了面向对象编程和函数式编程的优点,是STL算法中非常重要的组成部分。

函数对象 vs 普通函数

cpp
// 普通函数
bool isGreater(int a, int b) {
return a > b;
}

// 函数对象
struct IsGreater {
bool operator()(int a, int b) const {
return a > b;
}
};

// 使用方式
int main() {
// 使用普通函数
bool result1 = isGreater(5, 3); // 调用函数

// 使用函数对象
IsGreater isGreaterObj;
bool result2 = isGreaterObj(5, 3); // 调用函数对象

return 0;
}

函数对象的优势

函数对象相比普通函数有以下几个主要优势:

  1. 可以保存状态 - 函数对象可以包含成员变量,从而在多次调用之间保存状态
  2. 可以作为模板参数 - STL算法广泛使用函数对象作为模板参数
  3. 内联效率更高 - 编译器更容易对函数对象进行内联优化
  4. 类型安全 - 编译时类型检查更严格
  5. 可以携带类型信息 - 通过typedef等方式提供类型信息

标准库中的函数对象

C++ STL在<functional>头文件中定义了许多常用的函数对象:

算术函数对象

cpp
#include <functional>
#include <iostream>

int main() {
std::plus<int> add;
std::minus<int> subtract;
std::multiplies<int> multiply;
std::divides<int> divide;
std::modulus<int> modulo;
std::negate<int> negate;

std::cout << "加法: " << add(5, 3) << std::endl; // 输出: 8
std::cout << "减法: " << subtract(5, 3) << std::endl; // 输出: 2
std::cout << "乘法: " << multiply(5, 3) << std::endl; // 输出: 15
std::cout << "除法: " << divide(6, 3) << std::endl; // 输出: 2
std::cout << "取模: " << modulo(5, 3) << std::endl; // 输出: 2
std::cout << "取反: " << negate(5) << std::endl; // 输出: -5

return 0;
}

关系函数对象

cpp
#include <functional>
#include <iostream>

int main() {
std::equal_to<int> equal;
std::not_equal_to<int> not_equal;
std::greater<int> greater;
std::less<int> less;
std::greater_equal<int> greater_equal;
std::less_equal<int> less_equal;

std::cout << "等于: " << equal(5, 5) << std::endl; // 输出: 1
std::cout << "不等于: " << not_equal(5, 3) << std::endl; // 输出: 1
std::cout << "大于: " << greater(5, 3) << std::endl; // 输出: 1
std::cout << "小于: " << less(3, 5) << std::endl; // 输出: 1
std::cout << "大于等于: " << greater_equal(5, 5) << std::endl; // 输出: 1
std::cout << "小于等于: " << less_equal(3, 3) << std::endl; // 输出: 1

return 0;
}

逻辑函数对象

cpp
#include <functional>
#include <iostream>

int main() {
std::logical_and<bool> logical_and;
std::logical_or<bool> logical_or;
std::logical_not<bool> logical_not;

std::cout << "逻辑与: " << logical_and(true, false) << std::endl; // 输出: 0
std::cout << "逻辑或: " << logical_or(true, false) << std::endl; // 输出: 1
std::cout << "逻辑非: " << logical_not(true) << std::endl; // 输出: 0

return 0;
}

自定义函数对象的最佳实践

1. 保持函数对象小而专注

每个函数对象应该只做一件事,并做好这一件事。

cpp
// 好的做法 - 专注于一个功能
struct IsEven {
bool operator()(int num) const {
return num % 2 == 0;
}
};

// 不好的做法 - 试图做太多事情
struct NumberChecker {
bool operator()(int num, bool checkEven, bool checkPositive) {
if (checkEven && checkPositive)
return num % 2 == 0 && num > 0;
else if (checkEven)
return num % 2 == 0;
else if (checkPositive)
return num > 0;
return false;
}
};

2. 使用const标记不修改内部状态的操作符

cpp
struct Counter {
int count = 0;

// 修改内部状态,不用const
int operator()(int value) {
return count += value;
}
};

struct Multiplier {
int factor;

explicit Multiplier(int f) : factor(f) {}

// 不修改内部状态,使用const
int operator()(int value) const {
return value * factor;
}
};

3. 优先使用函数对象而非函数指针

cpp
#include <algorithm>
#include <vector>
#include <iostream>

// 使用函数指针
bool compareAsc(int a, int b) {
return a < b;
}

// 使用函数对象
struct CompareDesc {
bool operator()(int a, int b) const {
return a > b;
}
};

int main() {
std::vector<int> v1 = {5, 2, 8, 1, 9};
std::vector<int> v2 = {5, 2, 8, 1, 9};

// 使用函数指针排序
std::sort(v1.begin(), v1.end(), compareAsc);

// 使用函数对象排序 (更好的选择)
CompareDesc compareDesc;
std::sort(v2.begin(), v2.end(), compareDesc);

// 输出结果
std::cout << "升序排序: ";
for (int num : v1) std::cout << num << " "; // 输出: 1 2 5 8 9

std::cout << "\n降序排序: ";
for (int num : v2) std::cout << num << " "; // 输出: 9 8 5 2 1

return 0;
}

4. 提供类型定义以增强可读性

cpp
struct DivideBy {
double divisor;

explicit DivideBy(double d) : divisor(d) {}

// 定义类型以增强可读性
using result_type = double;
using first_argument_type = double;

result_type operator()(first_argument_type value) const {
return value / divisor;
}
};

5. 考虑状态可变的函数对象

cpp
#include <iostream>
#include <vector>
#include <algorithm>

class AccumulatingCounter {
private:
int total;

public:
AccumulatingCounter() : total(0) {}

// 函数调用时累加计数
int operator()(int x) {
return total += x;
}

// 获取当前累计值
int getTotal() const {
return total;
}

// 重置计数器
void reset() {
total = 0;
}
};

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
AccumulatingCounter counter;

std::for_each(numbers.begin(), numbers.end(), std::ref(counter));
std::cout << "累加总和: " << counter.getTotal() << std::endl; // 输出: 15

counter.reset();
std::cout << "重置后: " << counter.getTotal() << std::endl; // 输出: 0

return 0;
}

实际应用案例

案例1: 自定义排序规则

cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

// 定义一个学生结构体
struct Student {
std::string name;
int age;
double grade;

Student(const std::string& n, int a, double g)
: name(n), age(a), grade(g) {}
};

// 按年龄排序的函数对象
struct CompareByAge {
bool operator()(const Student& a, const Student& b) const {
return a.age < b.age;
}
};

// 按成绩排序的函数对象
struct CompareByGrade {
bool operator()(const Student& a, const Student& b) const {
return a.grade > b.grade; // 注意这里是降序排列
}
};

int main() {
std::vector<Student> students = {
{"张三", 20, 85.5},
{"李四", 19, 92.0},
{"王五", 22, 78.5},
{"赵六", 18, 88.0}
};

// 按年龄排序
std::sort(students.begin(), students.end(), CompareByAge());

std::cout << "按年龄升序排列的学生:" << std::endl;
for (const auto& student : students) {
std::cout << student.name << " - 年龄:" << student.age
<< " - 成绩:" << student.grade << std::endl;
}

// 按成绩排序
std::sort(students.begin(), students.end(), CompareByGrade());

std::cout << "\n按成绩降序排列的学生:" << std::endl;
for (const auto& student : students) {
std::cout << student.name << " - 年龄:" << student.age
<< " - 成绩:" << student.grade << std::endl;
}

return 0;
}

输出:

按年龄升序排列的学生:
赵六 - 年龄:18 - 成绩:88
李四 - 年龄:19 - 成绩:92
张三 - 年龄:20 - 成绩:85.5
王五 - 年龄:22 - 成绩:78.5

按成绩降序排列的学生:
李四 - 年龄:19 - 成绩:92
赵六 - 年龄:18 - 成绩:88
张三 - 年龄:20 - 成绩:85.5
王五 - 年龄:22 - 成绩:78.5

案例2: 数据转换

cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

// 可配置的温度转换函数对象
class TemperatureConverter {
private:
double factor;
double offset;
std::string unitName;

public:
// 构造函数设置转换参数
TemperatureConverter(double f, double o, const std::string& unit)
: factor(f), offset(o), unitName(unit) {}

// 转换温度值
double operator()(double celsius) const {
return celsius * factor + offset;
}

// 获取单位名称
std::string getUnit() const {
return unitName;
}
};

int main() {
std::vector<double> celsiusTemps = {0.0, 10.0, 20.0, 30.0, 100.0};

// 创建转换函数对象
TemperatureConverter toFahrenheit(9.0/5.0, 32.0, "°F");
TemperatureConverter toKelvin(1.0, 273.15, "K");

// 转换并打印华氏温度
std::cout << "摄氏温度转华氏温度:" << std::endl;
for (double temp : celsiusTemps) {
std::cout << temp << "°C = " << toFahrenheit(temp)
<< toFahrenheit.getUnit() << std::endl;
}

// 转换并打印开尔文温度
std::cout << "\n摄氏温度转开尔文温度:" << std::endl;
for (double temp : celsiusTemps) {
std::cout << temp << "°C = " << toKelvin(temp)
<< toKelvin.getUnit() << std::endl;
}

return 0;
}

输出:

摄氏温度转华氏温度:
0°C = 32°F
10°C = 50°F
20°C = 68°F
30°C = 86°F
100°C = 212°F

摄氏温度转开尔文温度:
0°C = 273.15K
10°C = 283.15K
20°C = 293.15K
30°C = 303.15K
100°C = 373.15K

使用Lambda表达式作为函数对象

C++11引入了Lambda表达式,它提供了一种更简洁的方式来创建匿名函数对象。

cpp
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 3, 5, 7, 2, 4, 6};

// 使用lambda表达式查找第一个偶数
auto it = std::find_if(numbers.begin(), numbers.end(),
[](int x) { return x % 2 == 0; });

if (it != numbers.end()) {
std::cout << "第一个偶数是:" << *it << std::endl; // 输出: 2
}

// 使用lambda表达式累加所有大于3的数
int sum = 0;
std::for_each(numbers.begin(), numbers.end(),
[&sum](int x) { if (x > 3) sum += x; });

std::cout << "所有大于3的数之和:" << sum << std::endl; // 输出: 22 (4+5+6+7)

return 0;
}
备注

虽然Lambda表达式使用起来更加简洁,但传统的函数对象在某些情况下仍然具有优势,比如代码复用、复杂的状态管理等场景。

函数对象与STL算法的结合

函数对象经常与STL算法一起使用,下面是几个常见例子:

cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

int main() {
std::vector<int> v = {5, 2, 8, 10, 3, 1, 6, 9, 4, 7};

// 使用标准函数对象排序
std::sort(v.begin(), v.end(), std::greater<int>());

std::cout << "降序排列: ";
for (int i : v) std::cout << i << " "; // 输出: 10 9 8 7 6 5 4 3 2 1
std::cout << std::endl;

// 使用函数对象计数
int count = std::count_if(v.begin(), v.end(),
[](int x) { return x > 5; });
std::cout << "大于5的元素个数: " << count << std::endl; // 输出: 5

// 使用函数对象变换数据
std::vector<int> squares(v.size());
std::transform(v.begin(), v.end(), squares.begin(),
[](int x) { return x * x; });

std::cout << "平方后: ";
for (int i : squares) std::cout << i << " "; // 输出: 100 81 64 49 36 25 16 9 4 1
std::cout << std::endl;

return 0;
}

总结

函数对象是C++中非常强大的工具,它结合了面向对象和函数式编程的优点。通过遵循这些最佳实践,你可以有效地利用函数对象来编写更加灵活、高效和可维护的代码:

  1. 保持函数对象小而专注
  2. 合理使用const关键字
  3. 优先选择函数对象而非函数指针
  4. 提供清晰的类型定义
  5. 合理利用状态管理功能
  6. 恰当地结合Lambda表达式
  7. 熟练掌握STL提供的标准函数对象
提示

记住,函数对象不仅仅是函数的替代品,它们是具有额外功能的强大工具。通过合理使用,可以编写出更加高效、灵活的代码。

练习

  1. 创建一个函数对象,用于检查一个数是否在指定范围内
  2. 使用标准库函数对象实现一个简单的计算器
  3. 创建一个可以统计特定条件下元素出现次数的函数对象
  4. 结合STL算法和自定义函数对象,实现一个学生成绩管理系统
  5. 比较使用函数指针、函数对象和Lambda表达式实现相同功能的性能差异

额外资源