C++(标准库):31---STL函数对象之(函数对象的概念及使用)
原创
©著作权归作者所有:来自51CTO博客作者董哥的黑板报的原创作品,请联系作者获取转载授权,否则将追究法律责任
一、函数对象(Function Object)概述
- 仿函数(functors)是早期的命名,C++标准规则定案后所采用的的新名称是函数对象(function objects)
- 所谓函数对象,就是一个定义了operator()的对象
函数对象就是一个“行为类似函数”的对象
- 函数的调用需要使用小括号进行调用。为了能够达到“行为类似函数”的目的,函数对象必须自定义(或者说重载、改写)function call运算子(operator())
- 拥有这样的运算子后,我们就可以在仿函数的 对象后面加上一对小括号,以此来调用函数对象所定义的operator()
- 例如下面是一个函数调用:
void function(int x, int y);
int main()
{
function(1, 2);
}
class X
{
public:
void operator()(int x, int y);
};
int main()
{
X fo;
fo(1, 2);
}
class Y
{
public:
void operator()();
};
int main()
{
Y po;
po();
}
- 如果类含有构造函数,那么使用函数对象前需要先使用构造函数构造对象。例如:
class Z
{
private:
int value;
public:
Z(int initialize) :value(initialize) {}
void operator()(int elem);
};
int main()
{
Z(10)(3);
}
二、函数对象的使用场景
- 函数对象可以运用在算法、容器中,也可以单独使用
- 并且标准库还预先定义好了一些函数对象,在下一篇文章介绍
三、函数对象相比函数的优点
①函数对象是一种带状态的函数
- “行为像pointer”的对象我们称之为智能指针,同理,“行为像function”的对象我们称之为函数对象
- 函数对象的能力超越了operator。函数对象可拥有成员函数和成员变量,这意味着函数对象拥有状态:
- 事实上,在同一时间点,相同类型的两个不同的函数对象所表述的相同机能,可具备不同的状态。这在寻常函数是不可能的
- 另一个好处是,你可以在运行期初始化它们——当然必须在它们被使用(被调用)之前
- 演示案例:如果我们需要将vector内的每个元素都加上特定的值。如果不使用函数对象,而使用函数模板,那么代码如下,这个方案的主要缺点是:
- 针对于每个函数模板的调用,我们需要为其每一份实例都生成一个实例化,因此下面为生成两份add()函数的实例定义
- 这种方法很不好,因为如果以后调用其他版本的add()函数,那么还需要生成其他版本的实例化,这样的话代码就十分的冗余
template<int theValue>
void add(int& elem)
{
elem += theValue;
}
int main()
{
vector<int> coll{ 1,2,3,4,5,6,7,8 };
for_each(coll.begin(), coll.end(), add<10>);
for_each(coll.begin(), coll.end(), add<20>);
}
- 演示案例:如果改用函数对象,那么就方便很多。相比于函数的优点如下:
- for_each()每次调用时都会创建一个临时函数对象,这些对象都有自己的状态,但是它们都是由同一种类型定义而来,代码不会冗余
class AddValue
{
private:
int theValue;
public:
AddValue(int v) :theValue(v) {}
void operator()(int &elem)const
{
elem += theValue;
}
};
int main()
{
vector<int> coll{ 1,2,3,4,5,6,7,8 };
for_each(coll.begin(), coll.end(), AddValue(10));
创建一个AddValue临时对象给for_each,临时对象的theValue=20
for_each(coll.begin(), coll.end(), AddValue(20));
}
②每个函数对象有其自己的类型
- 普通函数,唯有在其签名式不同时,才算类型不同。而函数对象即使签名式相同,也可以有不同的类型
- 事实上由函数对象定义的每一个函数行为都有其自己的类型。这对于“运用template实现泛型编程”乃是一个卓越的贡献,因为这么一来我们便可以将函数行为当做template参数来运用。这使得不同类型的容器可以使用同类型的函数对象作为排序准则。也可确保你不会在“排序准则不同”的集合间赋值、合并或比较
- 你甚至可以设计函数对象的继承体系,以此完成某些特别事情,例如在一个总体原则下确立某些特殊情况
③函数对象通常比寻常函数速度快
- 就template而言,由于更多细节在编译器就已经确定,所以畅通可能进行更好的优化。所以,传入一个函数对象(而非寻常函数)可能获得更好的执行效能
四、演示案例(将函数对象作为容器的排序准则)
使用预定义的函数对象
- set容器在创建时,如果不指定参数2,那么set容器采用默认的排序方法(升序)对容器内的元素进行排序。例如:
set<int> _set{ 0,3,1,4,2,5 };
for (const auto& val : _set)
{
std::cout << val << " ";
}
std::cout << std::endl;
- 如果我们创建set时,为其参数2指定一个函数对象,让其对其中的元素进行降序排序
- 其中std::greater是系统预定义的函数对象,在后面一篇文章介绍
set<int, std::greater<int>> _set{ 0,3,1,4,2,5 };
for (const auto& val : _set)
{
std::cout << val << " ";
}
std::cout << std::endl;
使用自定义的函数对象
- 例如下面有一个Person类,其存储我们的数据。另外定义一个PersonSortCriterion类,其能够创建函数对象,并且可以对Person进行排序
- 代码如下:
class Person
{
public:
std::string firstname()const { return _firstName; }
std::string lastname()const { return _lastName; }
private:
std::string _firstName;
std::string _lastName;
};
class PersonSortCriterion
{
public:
bool operator()(const Person&lhs, const Person& rhs)const
{
return (
(lhs.firstname() < rhs.firstname()) ||
(lhs.firstname() == rhs.firstname() && lhs.lastname() < rhs.lastname())
);
}
};
int main()
{
set<Person> coll1;
set<Person, PersonSortCriterion> coll2;
}
- coll2那个set,其在内部会每次调用两个Person对象,然后调用PersonSortCriterion.operator()运算符比较两个Person对象,然后将其保存到set容器中
五、函数对象拥有内部状态
- 下面展示function object如何能够“行为像个函数同时又拥有多个状态”:
演示案例①
class IntSequence
{
private:
int value;
public:
IntSequence(int initialValue) :value(initialValue) {}
int operator()()
{
return ++value;
}
};
int main()
{
vector<int> coll;
generate_n(back_inserter(coll), 9, IntSequence(1));
for (const auto& elem : coll)
{
std::cout << elem << " ";
}
std::cout << std::endl;
generate(next(coll.begin()), prev(coll.end()), IntSequence(42));
for (const auto& elem : coll)
{
std::cout << elem << " ";
}
}
- generate_n():调用参数3产生新值,并将新值赋值给以参数1起始的区间内的前参数2个元素
- generate():调用参数3产生新值,并将新值赋值给[参数1,参数2)所在区间内的元素
- 程序运行结果如下图所示:
- 上面的演示案例①调用的函数对象是by value传递给算法的:
- 优点是:你可以传递常量表达式或暂态表达式
- 缺点是:你无法改变function object的状态,因为是by value传递的,所以每次传递给算法时,算法操作结束之后,function object的状态仍与传入算法前一致(不论该function object是外部创建的,还是算法临时创建的)
- 有三个办法可以从“运用了function object”的算法中获得结果:
- ①在外部持有状态,并让function object指向它
- ②以by reference方式传递function object(见下面演示案例②)
- ③利用for_each()算法的返回值(下面“六”介绍)
演示案例②(by reference方式传递function object)
- 为了以by reference方式传递function object,你需要在调用算法时明示function object是个reference类型
- 代码如下:
- 第一次调用generate_n()时function object seq是以by reference方式传递
- 第二次调用generate_n()时是创建一个临时对象,并且在seq尾后进行插入4个元素,因此与seq无关
- 第三次调用generate_n()时是by value方式传递seq,因此seq的状态没有改变(其内部的value没有增加,还是为6)
- 第四次调用generate_n()时,因为第三次调用seq的状态没有改变(其内部的value没有增加,还是为6),所以还是从6开始插入
六、for_each的返回值
- 在“五”中我们介绍过,如果想要算法改变function object的状态,那么有三种方法,其中一种是for_each()
- for_each()语法参阅
- 使用for_each()算法,就不必费神以by reference方式传递function object,因为for_each()算法会返回参数3(它已在算法内部被改动过)的一个拷贝(副本)
演示案例
class MeanValue
{
private:
long num;
long sum;
public:
MeanValue() :num(0), sum(0) {}
void operator()(int elem)
{
num++;
sum += elem;
}
double value() const{
return static_cast<double>(sum) / static_cast<double>(num);
}
};
int main()
{
vector<int> coll{ 1,2,3,4,5,6,7,8 };
MeanValue mv = for_each(coll.begin(), coll.end(), MeanValue());
std::cout << "mean value: " << mv.value();
}
- 我们将MeanValue()临时对象传递给for_each(),整个算法执行过程中都是用这个临时对象的operator(),最后将MeanValue临时对象进行返回
- 这个演示案例在“for_each()”一文中也介绍过,稍有不同,但是原理一致
- 也可以使用lambda完成任务,并以by reference方式传递返回值。然而在这种情形下lambda不见得比较好,因为function object比较方便,例如当我们需要为associative或unordered容器声明一个hash函数或一个排序准则或相等准则。Function object通常是全局性的,这一事实有利于我们把它放入头文件或程序库,而lambda则是方便局部性地指明行为
七、Predicate(判别式)与函数对象