文章目录
一、引言
在C++编程中,函数的调用和处理通常涉及到多种不同的技术,如函数指针、函数对象(也称为仿函数或functor)、lambda表达式、bind 创建的对象以及重载了函数调用运算符的类。然而,这些技术都有其局限性,例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等 。因此,C++11提出std::function
和 std::bind
特性。
std::function
是一个通用的、多态的函数包装器,它可以保存、复制和调用任何Callable目标——函数、lambda表达式、bind表达式或其他函数对象。这使得我们能够以统一的方式处理各种可调用对象,无需关心它们的具体类型或签名。通过std::function
,我们可以将函数作为参数传递给其他函数,或者将函数存储在容器中,实现更为灵活和模块化的代码设计。
而std::bind
则是一种生成可调用对象的方式,它可以将一个可调用对象与其参数绑定在一起,生成一个新的可调用对象。这个新的可调用对象在调用时会将绑定的参数传递给原始的可调用对象
二、std::function基础
1、std::function的定义与声明
std::function
是C++标准库中的一个重要组件,std::function
在头文件<functional>
。用于封装任何可调用的目标(如函数、lambda表达式、函数对象等),并提供统一的调用语法。std::function
的类型仅取决于其调用签名,而不取决于封装的具体可调用目标的类型。这使得 std::function
非常适合作为回调机制、策略模式等场景中的通用接口。
1.std::function
的定义与声明
std::function
的模板参数通常是一个函数签名,即返回类型后跟一个参数包。例如,std::function<int(int, int)>
可以封装任何接受两个 int
参数并返回 int
的可调用目标。声明方式如下:
std::function<返回类型(参数类型)> func;
这里,返回类型表示封装的可调用对象的返回类型,而参数类型则表示其参数类型。你可以根据实际需要替换这些类型。例如,如果要封装一个接受两个整数参数并返回整数的函数,可以这样声明:
std::function<int(int, int)> func;
2. std::function
的模板参数
std::function
是一个模板类,其模板参数是一个可调用对象的签名,即它的返回类型和参数类型。
template<class Ret, class... Args>
class function<Ret(Args...)>;
Ret
是被调用的函数的返回类型,Args…
是被调用的函数的形参。
这个签名用于指定std::function
对象能够存储和调用的可调用对象的类型。例如,上述的std::function<int(int, int)>
就表示它可以存储一个接受两个int
参数并返回int
的可调用对象。
template <class T> function;
3. std::function
的构造函数与赋值
std::function
有多个构造函数和赋值操作符,用于初始化或修改其存储的可调用对象。以下是一些常见的操作形式:
-
默认构造函数:创建一个空的
std::function
对象,不存储任何可调用对象。std::function<int(int, int)> func; // 默认构造的空的std::function对象
-
通过可调用对象构造:用给定的可调用对象初始化
std::function
。void myFunc(int a, int b) { /* ... */ } std::function<void(int, int)> func1 = myFunc; // 存储函数指针 // 或者对于lambda表达式 auto lambda= [](int a, int b) { /* ... */ }; std::function<void(int, int)> func2 = lambda; // 存储lambda表达式
-
赋值操作:将一个可调用对象赋值给已存在的
std::function
对象。func = myFunc; // 将函数指针赋值给std::function func = lambda; // 将lambda表达式赋值给std::function
-
移动赋值操作:将一个
std::function
对象的内容移动到另一个std::function
对象,原对象变为空。std::function<void(int, int)> func1 = myFunc; std::function<void(int, int)> func2; func2 = std::move(func1); // func2现在存储了myFunc,而func1变为空
通过这些构造函数和赋值操作,std::function
能够灵活地处理各种可调用对象,使得在C++中处理函数和回调函数变得更加方便和统一。
2、std::function的基本使用
std::function
对象内部存储了一个指向封装的可调用目标的指针(或类似机制)。这个可调用目标的类型在实例化 std::function
时不需要指定,只需要知道其调用签名即可。
std::function
对象可以像普通函数那样调用,当调用时,它会转发调用到其封装的目标上。如果 std::function
对象没有封装任何目标(即它是空的),那么尝试调用它将抛出一个 std::bad_function_call
异常。
std::function
还提供了一些成员函数来检查其是否包含有效的可调用目标(例如,通过转换为 bool
)、获取封装的目标的类型信息、交换两个 std::function
对象封装的目标等。
非成员函数重载包括关系运算符,用于比较 std::function
对象和空指针(这通常用于检查 std::function
对象是否为空),以及一个 swap
函数,用于交换两个 std::function
对象封装的目标。
- 包装普通函数
假设我们有一个普通的函数,我们可以使用 std::function
来包装它:
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
std::function<int(int, int)> func = add;
int result = func(2, 3);
std::cout << "Result: " << result << std::endl; // 输出:Result: 5
return 0;
}
- 包装lambda表达式
lambda表达式在 C++11 中被引入,它提供了一种简洁的方式来定义匿名函数对象。我们可以很容易地使用 std::function
来包装 lambda表达式:
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::function<bool(int)> isEven = [](int n) { return n % 2 == 0; };
// 使用算法 std::remove_if 移除偶数
numbers.erase(std::remove_if(numbers.begin(), numbers.end(), isEven), numbers.end());
// 输出剩下的奇数
for (int num : numbers) {
std::cout << num << ' ';
} // 输出:1 3 5
return 0;
}
- 包装函数对象
函数对象(也称为仿函数)是重载了调用运算符 operator()
的类。我们可以使用 std::function
来包装它们:
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
class IsEven {
public:
bool operator()(int n) const {
return n % 2 == 0;
}
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::function<bool(int)> isEvenFunc = IsEven();
// 使用算法 std::remove_if 移除偶数
numbers.erase(std::remove_if(numbers.begin(), numbers.end(), isEvenFunc), numbers.end());
// 输出剩下的奇数
for (int num : numbers) {
std::cout << num << ' ';
} // 输出:1 3 5
return 0;
}
在上面的例子中,我们定义了一个名为 IsEven
的函数对象,并创建了一个 std::function
对象 isEvenFunc
来包装它。然后,我们像之前一样使用 std::remove_if
算法来移除偶数。
- 包装类的静态成员函数
静态成员函数与类关联,但不需要类的实例就可以调用。因此,包装静态成员函数相对简单。
#include <iostream>
#include <functional>
class MyClass {
public:
static void StaticFunc() {
std::cout << "Static function called." << std::endl;
}
};
int main() {
// 包装静态成员函数
std::function<void()> func = &MyClass::StaticFunc;
func(); // 调用静态函数
return 0;
}
- 包装类的成员函数
成员函数与类关联,但需要类的实例才可以调用。
#include <iostream>
#include <functional>
class MyClass {
public:
void memberFunction() {
std::cout << "Member function called." << std::endl;
}
};
int main() {
// 包装非静态成员函数
// 非静态成员函数需要对象的指针或者对象去进行调用
MyClass myClass;
function<void(MyClass*)> func1 = &MyClass::memberFunction;
func1(&myClass);
function<void(MyClass) > func2 = &MyClass::memberFunction;
func2(myClass);
return 0;
}
-
空的std::function对象
尝试调用一个空的
std::function
对象是未定义行为,通常会导致程序崩溃。因此,在调用std::function
对象之前,你应该检查它是否为空。
#include <iostream>
#include <functional>
void print_hello() {
std::cout << "Hello, world!" << std::endl;
}
int main() {
std::function<void()> func; // 默认构造一个空的 std::function 对象
if (!func)
std::cout << "func is empty." << std::endl;
func = print_hello; // 赋予一个可调用的目标
if (func)
func(); // 调用 func,输出 "Hello, world!"
func = nullptr; // 重新设置为空
if (!func)
std::cout << "func is empty again." << std::endl;
// 以下代码会导致未定义行为,因为 func 是空的
// func();
return 0;
}
std::function
的灵活性使得我们可以很容易地将不同的可调用对象作为参数传递给函数,从而编写出更加通用和可重用的代码。不过,需要注意的是,std::function
相比原生的函数指针或 lambda表达式,通常会有更大的开销,因为它需要动态地分配和存储调用的目标。因此,在性能敏感的场合中,需要谨慎使用。
三、std::bind基础
std::bind
是 C++ 标准库 <functional>
头文件中的一个功能,用于将可调用对象(如函数、成员函数、函数对象或 lambda表达式)与一组参数绑定在一起,生成一个新的可调用对象。这个新的对象在调用时会将之前绑定的参数传递给原始的函数或可调用对象。
1、std::bind的定义与声明
1. std::bind
的模板参数
std::bind
有两种形式:
- 不指定返回类型的版本:
template <class Fn, class... Args>
bind(Fn&& fn, Args&&... args);
- 指定返回类型的版本:
template <class Ret, class Fn, class... Args>
bind(Fn&& fn, Args&&... args);
这里,Fn
是被绑定的可调用对象(函数、函数对象、lambda表达式等)的类型,Args...
是可变模板参数,表示传递给 fn
的参数。**返回值是函数对象,不是一个函数指针。**即bind
函数会返回一个新的可调用对象,该对象在调用时会将参数转发给原始的可调用对象,并可能预先绑定一些参数。
在第一种形式中,返回类型由 fn
的返回类型决定。在第二种形式中,返回类型由模板参数 Ret
明确指定。如果指定了返回类型,它必须是唯一的、不能通过传递给 std::bind
的参数隐式推导出来的模板参数。
std::bind
的参数可以是值或占位符:
- 如果绑定到值,那么调用返回的函数对象时总是使用这个值作为参数。
- 如果使用占位符(如
std::placeholders::_1
、std::placeholders::_2
等),那么调用返回的函数对象时会将对应位置的参数转发给原始函数。
可以将bind
函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来“适应”原对象的参数列表。调用bind
的一般形式:
auto newCallable = bind(callable,arg_list);
其中,newCallable
本身是一个可调用对象,arg_list
是一个逗号分隔的参数列表,对应给定的 callable
的参数。当我们调用newCallable
时,newCallable
会调用callable
,并传给它arg_list
中 的参数。 arg_list
中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable
的参数,它们占据了传递给newCallable
的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
即:使用 std::bind
时,你可以指定哪些参数是预先绑定的,哪些参数是占位符(使用 std::placeholders::_N
,其中 N
是一个整数,表示参数的位置)。占位符在后续调用新生成的可调用对象时会被实际参数替换。
#include <iostream>
#include <functional>
void foo(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << std::endl;
}
int main() {
// 使用 std::bind 绑定 foo 函数的第一个参数为 10,第二个参数为 20
// 第三个参数使用占位符 _1,意味着调用时提供的第一个参数将填充到这里
auto bound_foo = std::bind(foo, 10, 20, std::placeholders::_1);
// 调用 bound_foo,传递第三个参数 30
bound_foo(30); // 输出: 10, 20, 30
return 0;
}
在上面的代码中,bound_foo
是一个新的可调用对象,它是通过 std::bind
创建的,将 foo
函数的第一个和第二个参数分别绑定为 10
和 20
,而第三个参数则留空,等待后续调用时提供。当我们调用 bound_foo(30)
时,实际上调用的是 foo(10, 20, 30)
。
2. std::bind
的赋值
std::bind
本身并不是一个类,因此它没有构造函数。相反,它是一个函数模板,用于生成一个新的可调用对象。这个新的可调用对象是通过调用 std::bind
函数来“构造”的,而不是通过传统意义上的构造函数。
关于赋值,std::bind
返回的可调用对象可以被赋值给一个函数指针类型兼容的可调用对象变量,或者赋值给任何接受这种类型参数的函数或算法。赋值操作与普通对象的赋值类似,但需要注意的是,由于 std::bind
返回的可调用对象通常是复杂且内部状态丰富的,因此赋值可能会导致某些非直观的行为,特别是当涉及到状态共享或移动语义时。
auto bound_foo1 = std::bind(foo, 1, std::placeholders::_1);
auto bound_foo2 = bound_foo1; // 赋值操作
bound_foo2(42); // 调用 bound_foo2,等同于调用 foo(1, 42)
在这个例子中,bound_foo1
被赋值给了 bound_foo2
,之后 bound_foo2
可以像 bound_foo1
一样被调用。但是需要注意的是,这里的赋值是浅复制,也就是说 bound_foo1
和 bound_foo2
指向的是相同的底层函数对象和数据结构。因此,在特定情况下(例如涉及到修改内部状态的操作时),需要谨慎处理这种赋值操作。
2、std::bind的基本使用
std::bind
的基本使用涵盖了绑定普通函数、成员函数以及带有占位符的参数。占位符 std::placeholders::_N
允许你在绑定时指定哪些参数应该在后续调用时提供。
- 绑定普通函数
当绑定普通函数时,你可以直接传递函数名和想要预先绑定的参数。
void print_sum(int a, int b) {
std::cout << a - b << std::endl;
}
int main() {
// 绑定 print_sum 函数的第二个参数为 5
auto bound_print_sum = std::bind(print_sum, std::placeholders::_1, 5);
// 调用 bound_print_sum,传入第一个参数 3
bound_print_sum(3); // 输出: -2
return 0;
}
在这个例子中,std::bind
创建了一个新的可调用对象 bound_print_sum
,它将 print_sum
函数的第二个参数绑定为 5
,第一个参数则留空,等待后续调用时提供。
- 绑定类的成员函数
当绑定成员函数时,你需要提供对象的指针或引用作为第一个参数(对于非静态成员函数),然后是想要预先绑定的参数。
#include <iostream>
#include <functional>
#include <string>
class MyClass {
public:
void print_with_prefix(const std::string& prefix, const std::string& message) {
std::cout << prefix << ": " << message << std::endl;
}
};
int main() {
MyClass obj;
// 绑定 obj 的 print_with_prefix 成员函数,并预先绑定 prefix 参数为 "Info"
auto bound_member_func = std::bind(&MyClass::print_with_prefix, &obj, "Info", std::placeholders::_1);
// 调用 bound_member_func,传入剩余的 message 参数
bound_member_func("Hello, world!"); // 输出: Info: Hello, world!
return 0;
}
在这个例子中,std::bind
创建了一个可调用对象 bound_member_func
,它将 MyClass
的 print_with_prefix
成员函数的 obj
对象和 prefix
参数绑定为 "Info"
,message
参数则留空等待后续调用时提供。
- 绑定类的静态成员函数
绑定静态成员函数与绑定普通函数类似,因为静态成员函数不需要类的实例即可调用。当你绑定静态成员函数时,你不需要提供对象实例的指针或引用,只需要提供函数本身和想要预先绑定的参数。
下面是一个绑定静态成员函数的例子:
#include <iostream>
#include <functional>
class MyClass {
public:
static void staticPrint(int a, int b) {
std::cout << "Static function called with: " << a << ", " << b << std::endl;
}
};
int main() {
// 绑定 MyClass 的静态成员函数 staticPrint,并预先绑定第一个参数为 5
auto bound_static_func = std::bind(MyClass::staticPrint, 5, std::placeholders::_1);
// 调用 bound_static_func,传入第二个参数 10
bound_static_func(10); // 输出: Static function called with: 5, 10
return 0;
}
在这个例子中,我们创建了一个 MyClass
类,其中包含一个静态成员函数 staticPrint
。我们使用 std::bind
来绑定这个静态成员函数,并将第一个参数预先绑定为 5
。由于 staticPrint
是一个静态成员函数,我们不需要提供类的实例来调用它。
然后,我们调用 bound_static_func
并传入剩余的第二个参数 10
。最终,MyClass::staticPrint
函数被调用,并输出预期的结果。
请注意,因为静态成员函数不依赖于类的实例,所以在 std::bind
的表达式中不需要包含对象指针或引用。这使得绑定静态成员函数比普通成员函数更简单和直接。
四、std::function通常与std::bind结合使用
std::function
和 std::bind
可以结合起来使用,以实现更为灵活和可复用的函数调用。std::function
是一个通用、多态的函数封装器,它可以存储、复制和调用任何可调用的目标——函数、lambda表达式、函数对象或其他函数指针。而 std::bind
用于绑定函数或成员函数到一个特定的对象实例,并可能预先绑定一些参数。
当 std::function
和 std::bind
结合使用时,你可以将 std::bind
生成的函数对象存储在 std::function
中,并通过 std::function
来调用它。这种组合允许你创建更加通用和可配置的回调函数或函数对象。
下面是一个 std::function
和 std::bind
结合使用的例子:
#include <iostream>
#include <functional>
// 一个简单的函数
void simpleFunction(int x, int y) {
std::cout << "Function called with: " << x << ", " << y << std::endl;
}
class MyClass {
public:
// 一个成员函数
void memberFunction(int a) {
std::cout << "Member function called with: " << a << std::endl;
}
};
int main() {
// 创建一个 MyClass 的实例
MyClass obj;
// 使用 std::bind 绑定成员函数到 MyClass 的实例,并预先绑定一个参数
auto boundMemberFunction = std::bind(&MyClass::memberFunction, &obj, 10);
// 使用 std::function 来存储绑定的函数对象
std::function<void()> functionObj = boundMemberFunction;
// 调用 functionObj,它实际上会调用 MyClass 的 memberFunction
functionObj(); // 输出: Member function called with: 10
// 使用 std::bind 绑定普通函数,并预先绑定一个参数
auto boundSimpleFunction = std::bind(simpleFunction, 1, std::placeholders::_1);
// 将绑定的普通函数存储在另一个 std::function 对象中
std::function<void(int)> anotherFunctionObj = boundSimpleFunction;
// 调用 anotherFunctionObj,它实际上会调用 simpleFunction
anotherFunctionObj(2); // 输出: Function called with: 1, 2
return 0;
}
在这个例子中,我们首先创建了一个 MyClass
的实例 obj
,然后使用 std::bind
将 MyClass
的成员函数 memberFunction
绑定到 obj
的实例上,并预先绑定了第一个参数为 10
。接着,我们将这个绑定的函数对象存储在 std::function<void()>
类型的 functionObj
中。之后,通过调用 functionObj()
,我们实际上调用了 obj
的 memberFunction
成员函数。
同样地,我们也展示了如何绑定普通函数 simpleFunction
并将其存储在 std::function<void(int)>
类型的 anotherFunctionObj
中。通过调用 anotherFunctionObj(2)
,我们实际上调用了 simpleFunction
并传递了参数。
这种结合使用 std::function
和 std::bind
的方式提供了极大的灵活性,允许你在运行时动态地改变函数的行为,或者将函数作为参数传递给其他函数或类。然而,正如之前提到的,lambda表达式在现代 C++ 编程中通常更加常用,因为它们提供了更为简洁和直观的语法来创建匿名函数对象。