跳到主要内容

C++ 函数包装器

在C++编程中,函数包装器是一种强大的工具,它允许我们以统一的方式处理各种可调用实体,例如普通函数、函数指针、成员函数、函数对象(仿函数)以及lambda表达式。函数包装器为函数式编程风格在C++中的应用提供了基础。

什么是函数包装器?

函数包装器是一个对象,它包装了一个可调用的目标(如函数、函数指针或函数对象),并允许像使用普通函数一样使用它。C++标准库中最主要的函数包装器是std::function,它定义在<functional>头文件中。

备注

函数包装器不仅提供了更一致的接口,还允许存储、传递和操作具有特定签名的任何可调用实体。

std::function 基础

std::function是一个通用的多态函数包装器,是C++11引入的组件。它能够存储、复制和调用任何可调用目标,包括:

  • 普通函数
  • 成员函数
  • 函数指针
  • 函数对象/仿函数
  • Lambda表达式

基本语法

cpp
std::function<返回类型(参数类型列表)> 函数对象名;

示例:包装不同类型的可调用对象

cpp
#include <iostream>
#include <functional>

// 普通函数
int add(int a, int b) {
return a + b;
}

// 函数对象
struct Multiply {
int operator()(int a, int b) const {
return a * b;
}
};

int main() {
// 存储普通函数
std::function<int(int, int)> func1 = add;
std::cout << "函数调用结果: " << func1(3, 4) << std::endl; // 输出: 7

// 存储函数对象
std::function<int(int, int)> func2 = Multiply();
std::cout << "函数对象调用结果: " << func2(3, 4) << std::endl; // 输出: 12

// 存储Lambda表达式
std::function<int(int, int)> func3 = [](int a, int b) { return a - b; };
std::cout << "Lambda表达式调用结果: " << func3(3, 4) << std::endl; // 输出: -1

return 0;
}

输出结果:

函数调用结果: 7
函数对象调用结果: 12
Lambda表达式调用结果: -1

std::function的高级用法

1. 成员函数包装

std::function可以包装类的成员函数,但需要提供对象实例:

cpp
#include <iostream>
#include <functional>

class Calculator {
public:
int add(int a, int b) const {
return a + b;
}
};

int main() {
Calculator calc;

// 包装成员函数
// 注意:需要使用std::bind或lambda表达式提供对象实例
std::function<int(int, int)> adder = std::bind(&Calculator::add, &calc, std::placeholders::_1, std::placeholders::_2);

// 或使用lambda表达式
std::function<int(int, int)> adder2 = [&calc](int a, int b) { return calc.add(a, b); };

std::cout << "成员函数调用结果: " << adder(5, 3) << std::endl; // 输出: 8
std::cout << "Lambda包装成员函数调用结果: " << adder2(5, 3) << std::endl; // 输出: 8

return 0;
}

2. 作为函数参数

std::function可以作为函数参数使用,实现策略模式:

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

// 使用std::function作为参数
void processNumbers(const std::vector<int>& numbers,
std::function<void(int)> processor) {
for (int num : numbers) {
processor(num);
}
}

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

// 使用lambda表达式作为处理函数
processNumbers(numbers, [](int n) {
std::cout << n * n << " ";
}); // 输出: 1 4 9 16 25

std::cout << std::endl;

int sum = 0;
processNumbers(numbers, [&sum](int n) {
sum += n;
});
std::cout << "Sum: " << sum << std::endl; // 输出: Sum: 15

return 0;
}

函数包装器的性能考虑

虽然函数包装器提供了极大的灵活性,但它也带来了一些性能开销:

  1. 与直接函数调用相比,通过std::function调用函数会引入额外的间接层。
  2. 函数包装器可能需要在堆上分配内存以存储目标函数对象,特别是当捕获较大的上下文时。
警告

在对性能要求极高的场景(如实时系统或嵌入式系统)中,应当谨慎使用函数包装器,并进行适当的性能测试。

实际应用场景

1. 回调函数机制

cpp
#include <iostream>
#include <functional>
#include <string>

class Button {
private:
std::function<void()> onClick;
std::string label;

public:
Button(const std::string& lbl) : label(lbl) {}

void setClickHandler(std::function<void()> handler) {
onClick = handler;
}

void click() {
std::cout << "按钮 '" << label << "' 被点击" << std::endl;
if (onClick) {
onClick(); // 调用回调函数
}
}
};

int main() {
Button saveButton("保存");
Button cancelButton("取消");

saveButton.setClickHandler([]() {
std::cout << "保存文件操作执行中..." << std::endl;
// 保存文件的逻辑
});

cancelButton.setClickHandler([]() {
std::cout << "取消操作,返回主界面..." << std::endl;
// 取消操作的逻辑
});

// 模拟用户点击
saveButton.click();
cancelButton.click();

return 0;
}

输出:

按钮 '保存' 被点击
保存文件操作执行中...
按钮 '取消' 被点击
取消操作,返回主界面...

2. 命令模式实现

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

// 使用函数包装器实现命令模式
class CommandManager {
private:
std::vector<std::function<void()>> commands;
std::vector<std::function<void()>> undoCommands;

public:
void addCommand(std::function<void()> command, std::function<void()> undoCommand) {
commands.push_back(command);
undoCommands.push_back(undoCommand);
}

void executeLatest() {
if (!commands.empty()) {
commands.back()();
commands.pop_back();
}
}

void undoLatest() {
if (!undoCommands.empty()) {
undoCommands.back()();
undoCommands.pop_back();
}
}
};

// 示例: 文本编辑器
class TextEditor {
private:
std::string text;
CommandManager commandManager;

public:
TextEditor() : text("") {}

void addText(const std::string& newText) {
std::string oldText = text;
commandManager.addCommand(
[this, newText]() {
text += newText;
std::cout << "添加文本: '" << newText << "'" << std::endl;
std::cout << "当前文本: " << text << std::endl;
},
[this, oldText]() {
text = oldText;
std::cout << "撤销添加,恢复为: " << text << std::endl;
}
);
commandManager.executeLatest();
}

void deleteLastChar() {
if (text.empty()) return;

std::string oldText = text;
char lastChar = text.back();

commandManager.addCommand(
[this]() {
text.pop_back();
std::cout << "删除最后一个字符" << std::endl;
std::cout << "当前文本: " << text << std::endl;
},
[this, oldText]() {
text = oldText;
std::cout << "撤销删除,恢复为: " << text << std::endl;
}
);
commandManager.executeLatest();
}

void undo() {
commandManager.undoLatest();
}
};

int main() {
TextEditor editor;

editor.addText("Hello, ");
editor.addText("world!");
editor.deleteLastChar();
editor.undo(); // 撤销删除,恢复为 "Hello, world!"
editor.undo(); // 撤销添加 "world!",恢复为 "Hello, "

return 0;
}

输出:

添加文本: 'Hello, '
当前文本: Hello,
添加文本: 'world!'
当前文本: Hello, world!
删除最后一个字符
当前文本: Hello, world
撤销删除,恢复为: Hello, world!
撤销添加,恢复为: Hello,

总结

C++函数包装器,尤其是std::function,是现代C++编程中非常重要的组件,它让我们能够:

  1. 统一处理各种可调用对象:无论是函数、函数指针、函数对象还是Lambda表达式
  2. 实现回调机制:为事件驱动编程提供支持
  3. 支持函数式编程风格:方便地传递和操作函数
  4. 实现设计模式:如策略模式、命令模式等

虽然函数包装器带来了一些性能开销,但在大多数情况下,其提供的灵活性和代码清晰度的提升远远超过了这些开销。

练习

  1. 创建一个计算器类,使用std::function存储加、减、乘、除四种操作,并允许用户选择执行哪种操作。
  2. 实现一个简单的事件系统,允许对象注册和触发各种事件及其处理程序。
  3. 使用函数包装器实现一个简单的任务调度器,可以添加任务并在适当的时候执行它们。

进一步学习资源

  1. C++标准库中的<functional>头文件
  2. 函数式编程在C++中的应用
  3. 设计模式:策略模式、命令模式、观察者模式与函数包装器的结合使用
提示

理解和掌握函数包装器是现代C++编程的重要技能,它能帮助你写出更加灵活、可维护的代码!