1 可调用对象
C++ 中的可调用对象(Callable Objects)是指那些能够被调用执行的对象。这包括了函数、函数对象(也叫做仿函数,即重载了 operator() 的类或者结构体)、Lambda 表达式以及任何具有 operator() 的成员函数的对象。可调用对象在 C++ 标准库算法(如 std::for_each、std::transform 等)以及回调函数等场景中广泛使用。
1.1 函数作为可调用对象
任何普通的函数都可以作为可调用对象使用。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 普通函数
void print_number(int num) {
std::cout << num << std::endl;
}
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), print_number);
return 0;
}
1.2 函数对象(仿函数)
函数对象是通过重载operator()的类或者结构体实现的。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 函数对象(仿函数)
struct PrintNumber {
void operator()(int num) const {
std::cout << num << std::endl;
}
};
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
PrintNumber print;
std::for_each(numbers.begin(), numbers.end(), print);
return 0;
}
1.3 Lambda 表达式
C++11 引入了 Lambda 表达式,它可以很方便地定义匿名函数对象。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Lambda 表达式
auto print_lambda = [](int num) {
std::cout << num << std::endl;
};
std::for_each(numbers.begin(), numbers.end(), print_lambda);
return 0;
}
1.4 可调用对象的类型
在 C++ 中,可调用对象的类型通常不是直接可以使用的,但是可以使用 std::function 来包装它们,使得可调用对象的类型变得统一。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
// 普通函数
void print_number(int num) {
std::cout << num << std::endl;
}
// 函数对象(仿函数)
struct PrintNumber {
void operator()(int num) const {
std::cout << num << std::endl;
}
};
// 使用 std::function 包装可调用对象
void use_callable(const std::function<void(int)>& callable, int num) {
callable(num);
}
int main()
{
// 使用 std::function 包装函数
use_callable(print_number, 12);
// 使用 std::function 包装函数对象
use_callable(PrintNumber(), 12);
// 使用 std::function 包装 Lambda 表达式
use_callable([](int num) { std::cout << num << std::endl; }, 12);
return 0;
}
1.5 注意事项
使用可调用对象的注意事项如下:
- 当使用std::function时,注意其性能可能稍低于直接使用函数调用,因为它需要进行类型擦除。在性能敏感的代码中,直接使用函数调用可能更合适。
- Lambda 表达式非常灵活,可以捕获局部变量(通过值或引用),具有不同的返回类型,并且可以有默认参数等。
- 函数对象(仿函数)可以包含状态,这是普通函数和 Lambda 表达式所不具备的。
2 std::function
std::function 是 C++11 标准库中的一个通用、多态的函数封装器。它允许将任何可调用对象(如函数、Lambda 表达式、函数对象或绑定表达式等)作为参数传递,或者赋值给变量。std::function 的灵活性使其成为实现回调、事件驱动编程以及更高级的编程技术的有力工具。
2.1 基本用法
首先,需要包含 <functional> 头文件来使用 std::function。
cpp
#include <iostream>
#include <functional> // 包含 std::function
// 一个普通的函数
void print_hello() {
std::cout << "Hello, World!" << std::endl;
}
int main()
{
// 创建一个 std::function 对象,它可以调用没有参数的函数
std::function<void()> func = print_hello;
// 调用 func,这将调用 print_hello 函数
func(); // 输出: Hello, World!
return 0;
}
2.2 使用 Lambda 表达式
std::function 也可以用来封装 Lambda 表达式。
cpp
#include <iostream>
#include <functional>
int main()
{
// 创建一个 std::function 对象,它可以调用接受一个 int 参数并返回 void 的函数或 Lambda
std::function<void(int)> func;
// 定义一个 Lambda 表达式,并将其赋值给 func
func = [](int x) {
std::cout << "Value: " << x << std::endl;
};
// 调用 func,传递一个整数参数
func(42); // 输出: Value: 42
return 0;
}
2.3 使用成员函数
std::function 也可以用来包装类的成员函数,但这需要一些额外的处理,因为成员函数需要一个对象来调用。通常使用 std::bind 或者 Lambda 表达式来包装成员函数。
cpp
#include <iostream>
#include <functional>
class MyClass {
public:
void print_value(int x) {
std::cout << "Value in MyClass: " << x << std::endl;
}
};
int main()
{
MyClass obj;
// 使用 Lambda 表达式包装成员函数
std::function<void(int)> func = [&obj](int x) {
obj.print_value(x);
};
// 调用 func,这将调用 MyClass 的成员函数
func(100); // 输出: Value in MyClass: 100
return 0;
}
2.4 std::function 的类型擦除
std::function 的类型擦除是 std::function 能够封装任意可调用对象(如函数、Lambda 表达式、函数对象等)并统一调用的关键机制。类型擦除意味着在运行时,std::function 对象隐藏了它所包含的可调用对象的实际类型,使得可以以一种统一的方式处理它们。
(1)类型擦除的实现原理
std::function 内部通常使用小对象优化(Small Object Optimization, SOO)和类型擦除技术来存储和调用可调用对象。小对象优化是一种减少内存分配开销的技术,它允许 std::function 在内部直接存储较小的对象,避免动态内存分配。对于较大的对象,std::function 则会使用动态内存分配。
类型擦除则是通过虚函数和函数指针来实现的。std::function 内部维护了一个指向一个函数对象的指针,这个函数对象负责调用实际的可调用对象。这个函数对象通常是一个模板类,它根据封装的可调用对象的类型进行特化。当调用 std::function 对象时,它会通过这个函数对象的虚函数表找到正确的调用方式,然后间接调用实际的可调用对象。
(2)类型擦除的优缺点
优点:
- 统一接口:类型擦除使得 std::function 可以提供一个统一的接口来调用不同类型的可调用对象,简化了代码。
- 灵活性:由于可以封装任意可调用对象,std::function 提供了极大的灵活性,使得可以轻松地传递回调函数、实现策略模式等。
缺点:
- 性能开销:类型擦除涉及到虚函数表查找和可能的动态内存分配,这会导致一定的性能开销。在性能敏感的代码中,这可能会成为一个问题。
- 类型安全减弱:由于类型信息在运行时被隐藏,类型安全在一定程度上被削弱。这意味着在编译时无法检查 std::function 对象所封装的可调用对象的类型是否正确。
(3)使用注意事项
- 避免不必要的类型擦除:如果代码的性能非常关键,且可调用对象的类型已知且固定,那么最好直接使用具体的可调用对象类型,而不是使用 std::function 进行类型擦除。
- 注意性能影响:在使用 std::function 时,要意识到它可能带来的性能开销,并在必要时进行性能测试和优化。
- 确保可调用对象兼容性:传递给 std::function 的可调用对象必须满足其期望的签名(即参数类型和返回类型)。否则,在运行时调用时可能会出现错误。
(4)示例代码
下面是一个简单的示例代码,展示了 std::function 的类型擦除特性:
cpp
#include <iostream>
#include <functional>
void print_int(int x) {
std::cout << "Integer: " << x << std::endl;
}
void print_double(double x) {
std::cout << "Double: " << x << std::endl;
}
int main()
{
std::function<void(int)> func_int = print_int; // 封装一个接受 int 的函数
std::function<void(double)> func_double = print_double; // 封装一个接受 double 的函数
func_int(12); // 输出: Integer: 12
func_double(3.14); // 输出: Double: 3.14
// 尽管 func_int 和 func_double 的类型不同,但它们都是 std::function 对象,
// 这体现了类型擦除:可以以统一的方式处理它们。
return 0;
}
2.5 使用 std::function 作为回调函数
std::function 非常适合用作回调函数,允许传递任何满足特定签名的可调用对象。这在异步编程、事件处理以及需要灵活性的场景中特别有用。
cpp
#include <iostream>
#include <functional>
#include <thread> // 用于线程
#include <chrono> // 用于休眠
// 模拟一个异步操作,完成后调用回调函数
void async_operation(std::function<void()> callback) {
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(1));
// 异步操作完成后调用回调函数
if (callback) {
callback();
}
}
void on_operation_complete() {
std::cout << "Operation completed!" << std::endl;
}
int main()
{
// 使用普通函数作为回调函数
async_operation(on_operation_complete);
// 或者使用 Lambda 表达式
async_operation([]() {
std::cout << "Operation completed with lambda!" << std::endl;
});
// 等待足够的时间以确保异步操作完成
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
在这个例子中,async_operation 函数接受一个 std::function<void()> 类型的回调函数,并在模拟的异步操作完成后调用它。这允许传递任何满足无参数、无返回值要求的可调用对象。
2.6 std::function 与模板
std::function 可以与模板一起使用,以创建更通用的代码。
cpp
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
// 模板函数,接受一个 std::function 和一个容器
template <typename Container, typename Function>
void process_container(const Container& c, Function func) {
for (const auto& item : c) {
func(item);
}
}
void print_number(int n) {
std::cout << n << " ";
}
int main()
{
std::vector<int> numbers = { 1, 2, 3, 4, 5 };
// 使用 Lambda 表达式作为参数
process_container(numbers, [](int n) {
std::cout << n * n << " ";
});
std::cout << std::endl; // 输出: 1 4 9 16 25
// 使用普通函数作为参数
process_container(numbers, print_number);
std::cout << std::endl; // 输出: 1 2 3 4 5
return 0;
}
上面代码的输出为:
1 4 9 16 25
1 2 3 4 5
在这个例子中,process_container 是一个模板函数,它接受一个容器和一个 std::function。这使得可以对任何类型的容器和任何满足特定签名的可调用对象进行操作。
2.9 注意事项
使用 std::function 的注意事项如下:
- std::function 通常比直接调用函数或成员函数慢,因为它涉及到类型擦除和可能的动态内存分配。
- 当使用 std::function 传递参数时,请确保传递的可调用对象的类型与 std::function 所期望的类型匹配或兼容。
- std::function 可以为空(即不包含任何可调用对象)。在调用空 std::function 对象时,将抛出 std::bad_function_call 异常。因此,在调用 std::function 对象之前,通常使用 operator bool 检查其是否包含有效的可调用对象。
3 std::bind
std::bind 是 C++ 标准库中的一个功能,它用于将可调用对象(如函数、函数对象或 Lambda 表达式)与特定的参数绑定在一起,从而生成一个新的可调用对象。这个新的可调用对象可以在稍后被调用,且其调用行为等同于原始可调用对象与绑定参数的结合。
3.1 基本用法
std::bind 的基本语法如下:
cpp
auto new_callable = std::bind(callable, arg1, arg2, ...);
其中 callable 是要被绑定的可调用对象(函数、函数对象或 Lambda 表达式),arg1, arg2, ... 是要传递给 callable 的参数。std::bind 返回一个新的可调用对象 new_callable,它保存了 callable 和其参数的信息。
std::bind 的一个强大特性是支持占位符 std::placeholders::_1, std::placeholders::_2, ...,它们允许指定哪些参数在调用 new_callable 时应该被替换。这使得 std::bind 可以创建灵活的回调函数和延迟计算函数。
针对如下代码:
cpp
void print_int(int n) {
std::cout << n << " ";
}
std::function<void(int)> func_int1 = print_int;
std::function<void(int)> func_int2 = bind(print_int,std::placeholders::_1);
其中对于 std::function<void(int)> func_int1 = print_int; ,这里的 print_int 是一个接受一个 int 参数并且没有返回值的函数或可调用对象。通过直接将 print_int 赋值给 func_int1,创建了一个 std::function 对象,该对象封装了 print_int 的调用。当之后调用 func_int1(some_int) 时,实际上是调用了 print_int(some_int)。
这种方式的优点是简单、直接,并且通常比使用 std::bind 有更好的性能,因为 std::function 可以直接存储对 print_int 的引用或指针,而不需要额外的函数调用或对象来转发调用。
对于 std::function<void(int)> func_int2 = bind(print_int,std::placeholders::_1); ,这里的 std::bind 用于创建一个新的可调用对象,该对象将 print_int 函数的调用与给定的参数绑定在一起。std::placeholders::_1 是一个占位符,表示当新的可调用对象被调用时,它的第一个参数应该传递给 print_int。
3.2 绑定函数和参数
cpp
#include <iostream>
#include <functional>
void print_sum(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
int main()
{
// 绑定 print_sum 函数和参数 5, 10
auto bound_func = std::bind(print_sum, 5, 10);
// 调用 bound_func,输出 "Sum: 15"
bound_func();
return 0;
}
3.3 使用占位符
cpp
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
bool is_even(int n) {
return n % 2 == 0;
}
int main()
{
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 使用 std::bind 和占位符 _1 来创建一个新的可调用对象,该对象接受一个整数并检查它是否是偶数
auto is_even_binder = std::bind(is_even, std::placeholders::_1);
// 使用 std::remove_if 和绑定的可调用对象来移除所有偶数
numbers.erase(std::remove_if(numbers.begin(), numbers.end(), is_even_binder), numbers.end());
// 输出剩余的数字(奇数)
for (int num : numbers) {
std::cout << num << ' ';
}
std::cout << std::endl; // 输出: 1 3 5 7 9
return 0;
}
在这个例子中,std::bind(is_even, std::placeholders::_1) 创建了一个新的可调用对象 is_even_binder,它接受一个参数(由占位符 std::placeholders::_1 表示)并调用 is_even 函数。然后,将这个新的可调用对象传递给 std::remove_if,它遍历 numbers 容器并移除所有偶数。
3.4 绑定成员函数和对象
cpp
#include <iostream>
#include <functional>
#include <string>
class Greeter {
public:
void greet(const std::string& name) const {
std::cout << "Hello, " << name << "!" << std::endl;
}
};
int main()
{
Greeter greeter;
// 绑定 greeter 对象的 greet 成员函数和占位符
auto bound_member_func = std::bind(&Greeter::greet, &greeter, std::placeholders::_1);
// 调用 bound_member_func,输出 "Hello, World!"
bound_member_func("World");
return 0;
}
这个例子创建了一个 Greeter 类的实例 greeter,并使用 std::bind 将 greet 成员函数和占位符 std::placeholders::_1 绑定到一起。这样,当调用 bound_member_func 时,它会调用 greeter 对象的 greet 成员函数并支持传递一个参数。
3.5 绑定多个参数和占位符
cpp
#include <iostream>
#include <functional>
#include <vector>
#include <string>
void print_info(const std::string& name, int age, const std::string& message) {
std::cout << "Name: " << name << ", Age: " << age << ", Message: " << message << std::endl;
}
int main()
{
std::string name = "Alice";
int fixed_age = 30;
// 使用 std::bind 绑定 name 和 fixed_age,保留第三个参数为占位符
auto bound_print_info = std::bind(print_info, name, fixed_age, std::placeholders::_1);
// 调用 bound_print_info,并传递额外的消息字符串
bound_print_info("Hello, world!"); // 输出: Name: Alice, Age: 30, Message: Hello, world!
return 0;
}
在这个例子中,std::bind 被用来绑定 name 和 fixed_age 参数到 print_info 函数,而第三个参数(消息字符串)则被保留为占位符 std::placeholders::_1。这样,当 bound_print_info 被调用时,就可以传递一个额外的字符串参数来替换占位符。
3.6 注意事项
使用 std::bind 的注意事项如下:
- 性能开销:std::bind 在运行时可能引入一些额外的性能开销,因为它涉及到函数对象的创建和可能的堆分配(如果绑定的对象很大)。在性能敏感的应用中,直接使用函数指针或 Lambda 表达式可能更为高效。
- Lambda 表达式的替代:C++11 引入了 Lambda 表达式,它们通常比 std::bind 更加直观和灵活。Lambda 表达式可以直接捕获局部变量,并定义自己的参数列表,这使得它们在很多情况下成为 std::bind 的更好替代。
- 与 std::function 结合使用:std::bind 常与 std::function 结合使用,因为 std::function 可以存储任何可调用对象,包括通过 std::bind 创建的对象。这种组合允许你创建非常灵活和可重用的回调机制。
- C++17 之后的替代:在 C++17 及以后的版本中,std::invoke 和结构化绑定提供了更多的灵活性和效率,可以考虑使用这些新特性来替代 std::bind。