一、函数对象概念
在c++中,我们把所有能当作函数使用的对象统称为函数对象。它是实现operator()的任何类型,此运算符被称为调用运算符,当调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。C++标准库主要使用函数对象作为容器和算法内的排序条件。在多线程编程中,主要使用函数对象来实现参数的传递。
二、函数对象的优势(相比普通函数)
- 函数对象可包含状态
- 函数对象是一个类型,因此可用作模板参数
三、函数对象分类
3.1 函数类
一个函数类,即重载了()操作符的类。当用该类的对象调用此操作符时,其表现形式如同普通函数一般,因此取名函数类。
//类
class FuncObjType
{
public:
//重载()操作符
void operator()()
{
cout<<"hello C++"<<endl;
}
}
//普通函数
void val()
{
cout<<"Hello C++!"<<endl;
}
int main()
{
//实例化类对象
FuncObjType val;
//调用函数类
val();
//普通函数调用
val();
}
**注:**类FuncObjType中重载了()操作符,因此对于该类对象val,可以调用该操作符val().该调用语句跟调用普通函数完全一样。
**疑问:**既然用函数对象与调用普通函数有相同的效果,为什么还要搞这么麻烦定义一个类来使用函数对象?(主要原因有以下几点)
- 函数对象可以有自己的状态,我们可以在类中定义状态变量,这样一个函数对象在多次的调用中可以共享这个状态。但是函数调用没这种优势,除非它使用全局变量来保存状态。
- 函数对象有自己特有的类型,而普通函数无类型可言。这种特性对于使用C++标准库来说是至关重要的。这样我们在使用STL中的函数时,可以传递相应的类型作为参数来实例化相应的模板,从而实现我们自己定义的规则和定制自己的算法,如排序算法。
3.2 Lambda
3.2.1 Lambda产生背景
使用 STL 时,往往会大量用到函数对象,为此要编写很多函数对象类。有的函数对象类只用来定义了一个对象,而且这个对象也只使用了一次,编写这样的函数对象类就有点浪费,而且,定义函数对象类的地方和使用函数对象的地方可能相隔较远,看到函数对象,想要查看其 operator() 成员函数到底是做什么的也会比较麻烦。对于只使用一次的函数对象类,能否直接在使用它的地方定义呢?Lambda 表达式能够解决这个问题。使用 Lambda 表达式可以减少程序中函数对象类的数量,使得程序更加优雅。C++11的一大亮点就是引入了Lambda表达式。利用Lambda表达式,可以方便的定义和创建匿名函数。Lambda表达式通过在最前面的方括号[ ]来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了外部变量。类似参数传递方式(值传递、引入传递、指针传递),在Lambda表达式中,外部变量的捕获方式也有值捕获、引用捕获、隐式捕获。
3.2.2Lambda表达式的声明
格式如下:
//完整声明格式
[capture list] (params list) mutable exception-> return type { function body }
//忽略某些成分的声明格式
[capture list](params list)->return type{function body} //格式1
[capture list](params list){function body} //格式2
[capture list]{function body} //格式3
各项具体含义:
- capture list: 捕获外部变量列表
- params list: 形参列表
- mutable: 用来说明是否可以修改捕获的变量
- exception: 异常设定
- return type: 返回类型
- function body:函数体
格式说明:
- 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值
- 格式2省略了返回值类型,但是编译器根据以下规则能够推断出Lambda表达式的返回类型。(1)如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定.(2)如果function body中没有return语句,则返回值为void类型。
- 格式3中省略了参数列表,类似普通函数中的无参函数。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
bool cmp(int a, int b)
{
return a < b;
}
int main()
{
vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
vector<int> lbvec(myvec);
sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
cout << "predicate function:" << endl;
for (int it : myvec)
cout << it << ' ';
cout << endl;
sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; }); // Lambda表达式
cout << "lambda expression:" << endl;
for (int it : lbvec)
cout << it << ' ';
}
注:在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。
3.2.3捕获外部变量
Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明。Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了外部变量。
示例:
#include <iostream>
using namespace std;
int main()
{
int a = 123;
auto f = [a] { cout << a << endl; };
f(); // 输出:123
//或通过“函数体”后面的‘()’传入参数
auto x = [](int a){cout << a << endl;}(123);
}
**注:**上面这个例子先声明了一个整型变量a,然后再创建Lambda表达式,该表达式“捕获”了a变量,这样在Lambda表达式函数体中就可以获得该变量的值。
类似参数传递方式(值传递、引入传递、指针传递),在Lambda表达式中,外部变量的捕获方式也有值捕获、引用捕获、隐式捕获。
(1)值捕获
值捕获和参数传递中的值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入,因此随后对该变量的修改不会影响影响Lambda表达式中的值。
示例:
int main()
{
int a = 123;
auto f = [a] { cout << a << endl; };
a = 321;
f(); // 输出:123
}
注:如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该外部变量的值。
(2)引用捕获
使用引用捕获一个外部变量,只需要在捕获列表变量前面加上一个引用说明符&
示例:
int main()
{
int a = 123;
auto f = [&a] { cout << a << endl; };
a = 321;
f(); // 输出:321
}
**注:**从示例中可以看出,引用捕获的变量使用的实际上就是该引用所绑定的对象。
(3)隐式捕获
上面的值捕获和引用捕获都需要我们在捕获列表中显示列出Lambda表达式中使用的外部变量。除此之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。
隐式值捕获示例:
int main()
{
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕获
f(); // 输出:123
}
隐式引用捕获示例:
int main()
{
int a = 123;
auto f = [&] { cout << a << endl; }; // 引用捕获
a = 321;
f(); // 输出:321
}
(4)混合方式
上面的例子,要么是值捕获,要么是引用捕获,Lambda表达式还支持混合的方式捕获外部变量,这种方式主要是以上几种捕获方式的组合使用。
捕获方式 | 说明 |
---|---|
[ ] | 不捕获任何外部变量 |
[变量名, …] | 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符) |
[this] | 以值的形式捕获this指针 |
[=] | 以值的形式捕获所有外部变量 |
[&] | 以引用形式捕获所有外部变量 |
[=, &x] | 变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] | 变量x以值的形式捕获,其余变量以引用形式捕获 |
3.2.4修改捕获变量
在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。那么有没有办法可以修改值捕获的外部变量呢?这是就需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量。
示例:
int main()
{
int a = 123;
auto f = [a]()mutable { cout << ++a; }; // 不会报错
cout << a << endl; // 输出:123
f(); // 输出:124
}
3.2.5Lambda表达式的参数
Lambda表达式的参数和普通函数的参数类似,但是Lambda表达式中传递参数还有一些限制,主要有以下几点:
- 参数列表中不能有默认参数
- 不支持可变参数
- 所有参数必须有参数名
示例:
{
int m = [](int x) { return [](int y) { return y * 2; }(x)+6; }(5);
std::cout << "m:" << m << std::endl; //输出m:16
std::cout << "n:" << [](int x, int y) { return x + y; }(5, 4) << std::endl; //输出n:9
auto gFunc = [](int x) -> function<int(int)> { return [=](int y) { return x + y; }; };
auto lFunc = gFunc(4);
std::cout << lFunc(5) << std::endl;
auto hFunc = [](const function<int(int)>& f, int z) { return f(z) + 1; };
auto a = hFunc(gFunc(7), 8);
int a = 111, b = 222;
auto func = [=, &b]()mutable { a = 22; b = 333; std::cout << "a:" << a << " b:" << b << std::endl; };
func();
std::cout << "a:" << a << " b:" << b << std::endl;
a = 333;
auto func2 = [=, &a] { a = 444; std::cout << "a:" << a << " b:" << b << std::endl; };
func2();
auto func3 = [](int x) ->function<int(int)> { return [=](int y) { return x + y; }; };
std::function<void(int x)> f_display_42 = [](int x) { print_num(x); };
f_display_42(44);
}
3.3 std::function类(函数模板类)
当你需要一个非模板函数对象作为类的成员或者函数参数时,你必须指定函数对象的具体类型。C++中的函数对象并没有一个基类,但是标准库提供了一个模板类std::function来代表所有的函数对象。std::function 是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
- 定义格式:std::function<函数类型>。
- std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。
不同类型可能具有相同的调用形式,如:
// 普通函数
int add(int a, int b){return a+b;}
// lambda表达式
auto mod = [](int a, int b){ return a % b;}
// 函数对象类
struct divide{
int operator()(int denominator, int divisor){
return denominator/divisor;
}
};
上述三种可调用对象虽然类型不同,但是共享了一种调用形式:
int(int ,int)
std::function就可以将上述类型保存起来,如下:
std::function<int(int ,int)> a = add;
std::function<int(int ,int)> b = mod ;
std::function<int(int ,int)> c = divide();
- std::function并不像std::vector等容器对包含的类型做了抽象,而是抽象了函数对象的参数和返回值。无论是普通函数,还是函数指针、lambdas,又或是任何可以被当做函数使用的对象,只要拥有相同参数和返回值,均可以用同一类std::function表示。
- 尽管std::function非常有用,但是它也带来了性能损失。这是因为为了隐藏包含的函数对象类型,提供通用的调用接口,std::function使用了叫做type erasure的技术。简单来说是通过虚函数的调用在运行期来决定具体调用,因此编译器无法内联(inline)函数调用,也无法进行更多优化。
3.4 std::bind
可将std::bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。在函数式编程中,通过组合现有的函数,我们可以创造出新的函数。标准库中的std::bind就是可以创造闭包(closure)的工具。
std::bind主要有以下两个作用:
- 将可调用对象和其参数绑定成一个仿函数
- 只绑定部分参数,减少可调用对象传入的参数
3.1 std::bind()绑定普通函数
double my_divide (double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2);
std::cout << fn_half(10) << '\n'; //输出5
- bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind (my_divide,_1,2)等价于std::bind (&my_divide,_1,2)
- _1表示占位符,位于中,std::placeholders::_1表示参数的顺序,这里指定了第一个参数的位置在第一位,第二位给出了默认值,这样就减少了一个调用对象传入的参数。
3.4.1 std::bind()绑定成员函数
struct Foo {
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
int data = 10;
};
int main()
{
Foo foo;
auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
f(5); // 100
}
- bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
- 必须显示的指定&Foo::print_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Foo::print_sum前添加&;
- 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &foo;
3.4.2 std::bind()绑定一个引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,有时对有些绑定的参数希望以引用而不是拷贝的方式传递或者要传入的参数无法拷贝,就需要借助std::ref()函数。
eg1:想以引用方式传递参数
#include <functional>
void print2(int &a, int &b)
{
std::cout << "函数调用:a=" << a << ", b=" << b << "\n";
++a;
++b;
}
int main(int argc, char* argv[], char* env[])
{
int a = 1;
int b = 2;
auto func2 = std::bind(print2, a, std::ref(b));
std::cout << "调用前,a=" << a << ", b=" << b << "\n";
func2();
std::cout << "调用后,a=" << a << ", b=" << b << "\n";
return 0;
}
结果输出:
调用前,a=1,b=2
函数调用:a=1,b=2
调用后:a=1,b=3
eg2:传入的参数不能拷贝
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
#include <sstream>
using namespace std::placeholders;
using namespace std;
ostream & print(ostream &os, const string& s, char c)
{
os << s << c;
return os;
}
int main()
{
vector<string> words{"helo", "world", "this", "is", "C++11"};
ostringstream os;
char c = ' ';
for_each(words.begin(), words.end(),
[&os, c](const string & s){os << s << c;} );
cout << os.str() << endl;
ostringstream os1;
// ostream不能拷贝,若希望传递给bind一个对象,
// 而不拷贝它,就必须使用标准库提供的ref函数
for_each(words.begin(), words.end(),
bind(print, ref(os1), _1, c));
cout << os1.str() << endl;
}
- std::bind的函数参数默认使用的是拷贝, 如果需要使用引用,则需要配合std::ref。
官方例子
示例1:
#include <random>
#include <iostream>
#include <memory>
#include <functional>
void f(int n1, int n2, int n3, const int& n4, int n5)
{
std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}
int g(int n1)
{
return n1;
}
struct Foo {
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
int data = 10;
};
int main()
{
using namespace std::placeholders; // for _1, _2, _3...
// demonstrates argument reordering and pass-by-reference
int n = 7;
// (_1 and _2 are from std::placeholders, and represent future
// arguments that will be passed to f1)
auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
n = 10;
f1(1, 2, 1001); // 1 is bound by _1, 2 is bound by _2, 1001 is unused
// makes a call to f(2, 42, 1, n, 7)
// nested bind subexpressions share the placeholders
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // makes a call to f(12, g(12), 12, 4, 5);
// common use case: binding a RNG with a distribution
std::default_random_engine e;
std::uniform_int_distribution<> d(0, 10);
auto rnd = std::bind(d, e); // a copy of e is stored in rnd
for(int n=0; n<10; ++n)
std::cout << rnd() << ' ';
std::cout << '\n';
// bind to a pointer to member function
Foo foo;
auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
f3(5);
// bind to a pointer to data member
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << '\n';
// smart pointers can be used to call members of the referenced objects, too
std::cout << f4(std::make_shared<Foo>(foo)) << '\n'
<< f4(std::make_unique<Foo>(foo)) << '\n';
}
结果输出:
2 42 1 10 7
12 12 12 4 5
1 5 0 2 0 8 2 2 10 8
100
10
10
10
示例2:
#include <algorithm>
class Foo
{
public:
void methodA();
void methodInt(int a);
};
class Bar
{
public:
void methodB();
};
void main()
{
std::function<void()> f1; // 无参数,无返回值
Foo foo;
f1 = std::bind(&Foo::methodA, &foo);
f1(); // 调用 ();
Bar bar;
f1 = std::bind(&Bar::methodB, &bar);
f1(); // 调用 ();
f1 = std::bind(&Foo::methodInt, &foo, 42);
f1(); // 调用 (42);
std::function<void(int)> f2; // int 参数,无返回值
f2 = std::bind(&Foo::methodInt, &foo, _1);
f2(53); // 调用 (53);
}
通过std::bind,我们可以为同一个类的不同对象可以分派不同的实现,从而实现不同的行为。这种方式使得我们不在需要设计通过继承与虚函数来实现多态,无疑为程序库设计提供的新的方式。