【C++进阶】function和bind及可变模板参数
文章目录
- 1. function和bind
- 1.1 function使用方法
- 1.2 bind
- 2. 可变模板参数
- 2.1 可变模板参数函数
- 2.2 可变模板参数的展开
1. function和bind
C++中的function和bind是为了更方便地进行函数对象的封装和调用而设计的。
function是一个通用的函数对象容器,可以存储任意可调用对象(函数、函数指针、成员函数、lambda表达式等),并提供了一致的接口来调用这些对象。通过function,我们可以将一个函数或函数对象作为参数传递给其他函数或存储在容器中,实现更加灵活的编程。
bind则是一个用于将函数和其参数进行绑定的工具,可以将一个函数和部分参数绑定在一起,生成一个新的函数对象,这个新的函数对象可以像原函数一样进行调用,但会自动填充绑定的参数。通过bind,我们可以方便地实现函数的柯里化,即将一个多参数函数转化为一个单参数函数序列,提高代码的可读性和复用性。
综上,C++中的function和bind是为了更好地支持函数式编程和泛型编程而设计的,可以帮助我们更加方便地处理函数对象和参数绑定。
1.1 function使用方法
std::function
是一个通用的函数对象容器,可以存储任意可调用对象(函数、函数指针、成员函数、lambda表达式等),并提供了一致的接口来调用这些对象。function函数的语法如下:
template<class R, class... Args>
class function<R(Args...)>;
其中,R表示返回值类型,Args表示参数类型。function类模板的对象可以存储任何可调用对象,包括函数、函数指针、成员函数和lambda表达式等。
下面是function函数的几个用法示例:
- 存储函数指针
#include <iostream>
#include <functional>
void foo(int a, int b)
{
std::cout << "a = " << a << ", b = " << b << std::endl;
}
int main()
{
std::function<void(int, int)> f = foo;
f(1, 2); // 调用foo函数
return 0;
}
- 存储函数对象
#include <iostream>
#include <functional>
class Bar
{
public:
void operator()(int a, int b)
{
std::cout << "a = " << a << ", b = " << b << std::endl;
}
};
int main()
{
std::function<void(int, int)> f = Bar();
f(1, 2); // 调用Bar::operator()函数
return 0;
}
- 存储成员函数指针和对象指针
#include <iostream>
#include <functional>
class Baz
{
public:
void foo(int a, int b) const
{
std::cout << "a = " << a << ", b = " << b << std::endl;
}
};
int main()
{
std::function<void(const Baz&, int, int)> f = &Baz::foo;
Baz baz;
f(baz, 1, 2); // 调用Baz::foo函数
return 0;
}
- 存储lambda表达式
#include <iostream>
#include <functional>
int main()
{
std::function<void(int, int)> f = [](int a, int b) {
std::cout << "a = " << a << ", b = " << b << std::endl;
};
f(1, 2); // 调用lambda表达式
return 0;
}
在使用function时,需要注意几个问题:
- function对象可以被赋值为nullptr,表示该对象不再存储任何可调用对象。
- function对象可以被默认构造函数初始化,此时该对象不存储任何可调用对象。
- function对象可以被拷贝和移动,拷贝和移动后的对象存储的是相同的可调用对象。
- 调用function对象时,需要使用operator()函数,参数类型和返回值类型与function对象的模板参数一致。
- 如果function对象存储的是一个成员函数指针,需要在调用时传递对象指针作为第一个参数。
1.2 bind
std::bind
用于将函数对象和其参数进行绑定,生成一个新的函数对象,这个新的函数对象可以像原函数一样进行调用,但会自动填充绑定的参数。bind函数的语法如下:
template<class F, class... Args>
auto bind(F&& f, Args&&... args) -> std::function<typename std::result_of<F(Args...)>::type()>
其中,f是需要绑定的函数对象,args是需要绑定的参数。bind函数会返回一个新的函数对象,其参数类型和返回值类型都由原函数对象推导而来。
下面是bind函数的几个用法示例:
- 绑定函数和参数
#include <iostream>
#include <functional>
void foo(int a, int b, int c)
{
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}
int main()
{
auto f = std::bind(foo, 1, 2, 3);
f(); // 调用foo函数
return 0;
}
- 绑定成员函数和对象指针
#include <iostream>
#include <functional>
class Bar
{
public:
void foo(int a, int b, int c)
{
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}
};
int main()
{
Bar bar;
auto f = std::bind(&Bar::foo, &bar, 1, 2, 3);
f(); // 调用foo函数
return 0;
}
- 绑定函数对象和参数
#include <iostream>
#include <functional>
class Baz
{
public:
void operator()(int a, int b, int c)
{
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}
};
int main()
{
Baz baz;
auto f = std::bind(baz, 1, 2, 3);
f(); // 调用operator()函数
return 0;
}
- 绑定函数对象和部分参数
#include <iostream>
#include <functional>
int add(int a, int b, int c)
{
return a + b + c;
}
int main()
{
auto f = std::bind(add, 1, std::placeholders::_1, 3);
std::cout << f(2) << std::endl; // 调用add函数
return 0;
}
上面的例子中,std::placeholders::_1表示占位符,表示在调用f函数时,第一个参数将会被填充到占位符的位置上,而其他的参数则会按照绑定的顺序进行填充。
2. 可变模板参数
可变模板参数是C++11引入的新特性,允许模板参数的数量是可变的。使用可变模板参数可以更加灵活地定义模板类和函数,支持对不同数量的参数进行处理。
可变模板参数的语法如下:
template<typename... T>
void f(T... args);
上面的可变模板参数的定义当中,省略号的作用有两个:
- 声明一个参数包
T... args
,这个参数包中可以包含0到任意个模板参数 - 在模板定义的右边,可以将参数包展开成一个一个独立的参数
上面的参数args前面有省略号,所以它就是一个可变模板参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模板参数。我们无法直接获取参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
可变模板参数和普通的模板参数语义是一致的,所以可以应用于函数和类,即可变模板参数函数和可变模板参数类,然而,模板函数不支持偏特化,所以可变模板参数函数和可变模板参数类展开可变模板参数的方法还不尽相同,下面我们来分别看看他们展开可变模板参数的方法。
2.1 可变模板参数函数
#include <iostream>
using namespace std;
template <class... T>
void f(T... args)
{
cout << sizeof...(args) << endl; // 打印变参的个数
}
int main()
{
f(); // 0
f(1, 2); // 2
f(1, 2.5, ""); // 3
return 0;
}
上面的例子中,f()没有传入参数,所以参数包为空,输出的size为0,后面两次调用分别传入两个和三个参数,故输出的size分别为2和3。由于可变模版参数的类型和个数是不固定的,所以我们可以传任意类型和个数的参数给函数f。这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。
2.2 可变模板参数的展开
C++11和C++17中提供了不同的方法来展开可变模板参数,下面分别介绍这些方法:
- 递归展开
递归展开是指使用递归函数来逐一展开参数包。递归展开的基本思路是:先处理第一个参数,然后递归处理剩余的参数,直到参数包为空。
下面是一个使用递归展开的示例:
#include <iostream>
template<typename T>
void print(const T& value)
{
std::cout << value << std::endl;
}
template<typename T, typename... Args>
void print(const T& value, const Args&... args)
{
std::cout << value << std::endl;
print(args...);
}
int main()
{
print(1, 2.5, "hello", "world"); // 输出1, 2.5, hello, world
return 0;
}
在上面的代码中,我们使用print函数来展开参数包,当参数包非空时,调用print(args…)递归处理剩余的参数。
- 常规展开(逗号表达式)
常规展开是指使用逗号表达式和初始化列表来展开参数包。常规展开的基本思路是:将参数包中的每一个参数都用逗号隔开,放在一个初始化列表中,然后使用逗号表达式来对初始化列表进行展开。
下面是一个使用常规展开的示例:
#include <iostream>
template<typename... Args>
void print(const Args&... args)
{
int dummy[] = {(std::cout << args << std::endl, 0)...};
}
int main()
{
print(1, 2.5, "hello", "world"); // 输出1, 2.5, hello, world
return 0;
}
在上面的代码中的这种展开参数包的方式,不需要通过递归终止函数,是直接在print函数体中展开的,函数中的逗号表达式:(std::cout << args << std::endl, 0)
,先执行std::cout << args << std::endl
,再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(std::cout << args << std::endl, 0)...}
将会展开成(std::cout << arg1 << std::endl, 0), (std::cout << arg2 << std::endl, 0), (std::cout << arg3 << std::endl, 0), ...
,最终会创建一个元素值都为0的数组int dummy[sizeof…(args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分std::cout << args << std::endl
打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,具体代码如下:
#include <iostream>
template<typename F, typename... Args>
void print(const F& f, Args&&... args)
{
std::initializer_list<int> {(f(std::forward<Args>(args)), 0)...}; // 这里使用了完美转发
}
int main()
{
print([](int i){std::cout << i << std::endl;}, 1, 2, 3); // 因为initializer_list为int类型,故这里这里只能传入int类型的参数
return 0;
}
在上面的代码中,我们首先定义了一个initializer_list
(初始化列表)类型的对象,然后使用折叠表达式将参数包中的每一个参数都传递给函数对象f进行处理。在传递参数时,我们使用了完美转发,以保证传递的参数类型和值都正确。
在main函数中,我们使用print函数来输出整数1、2、3。具体来说,我们传递了一个lambda表达式,该表达式接收一个整数参数并将其输出到标准输出流中。然后我们传递了3个整数参数1、2、3,这些参数会被print函数展开并传递给lambda表达式进行处理。
需要注意的是,因为initializer_list为int类型,故这里只能传递int类型的参数。如果需要传递其他类型的参数,需要修改initializer_list的类型。
- 折叠表达式
折叠表达式是C++17中引入的新特性,可以方便地对参数包进行展开和折叠。折叠表达式的基本语法如下:
(expression op ... op pack)
其中,expression是一个表达式,op是一个二元操作符,pack是一个参数包。折叠表达式会将参数包中的每一个参数都应用于expression,并使用op进行折叠。
下面是一个使用折叠表达式的示例:
#include <iostream>
template<typename... Args>
void print(const Args&... args)
{
// (std::cout << ... << args) << std::endl; // 这个不会换行
((std::cout << args << '\n'), ...);
}
int main()
{
print(1, 2.5, "hello", "world"); // 输出1, 2.5, hello, world
return 0;
}
在上面的代码中,我们使用print函数来展开参数包,使用折叠表达式将参数包中的每一个参数都输出到标准输出流中。