5 - 仿函数
仿函数
在 Part1
的可调用对象
中已经提到了仿函数:可以通过重载类中的函数调用运算符()
,以便用类的对象代替函数指针。这些对象被称为函数对象/仿函数。
这里着重介绍一下标准库中的一些仿函数,它们通常定义在<functional>
中。
算术函数对象
C++为5种二元算术运算符提供了仿函数类模板:plus
, minus
, multiplies
, divides
, modulus
;还为提供了一元运算符negate
。
例如使用plus
类模板:
plus<int> myIntPlus;
cout << "Ans: " << myIntPlus(2, 3) << endl;
当然有人会想,为什么不直接用operator+
。算数函数对象的好处就是可以将它们直接作为回调转递给其他函数,而直接用算术运算符却不能这样。例如下例,定义函数calculateData()
,将容器内元素进行给定的Op
运算,初始值为startVal
:
template<typename Iter, typename StartVal, typename Operation>
auto calculateData(Iter begin, Iter end, StartVal startVal, Operation op)
{
auto result{ startVal };
for (Iter iter{ begin }; iter != end; ++iter)
{
result = op(result, *iter);
}
return result;
}
// main()
vector<int> iVec{ 1, 2, 3, 4};
cout << "plus: " << calculateData(cbegin(iVec), cend(iVec), 0, plus<int>{}) << endl;
cout << "Multiply: " << calculateData(cbegin(iVec), cend(iVec), 1, multiplies<int>{}) << endl;
需要注意的是:算术函数对象只是算术运算符的包装器,例如想要用plus<T>
,得先确保这个T
类型实现了operator+
。
使用透明运算符
在上例中,我们显式地给算术仿函数指定类型int
,但C++支持透明运算符仿函数,允许你忽略模板类型参数。例如可以指定plus<>()
为plus<void>()
而不是plus<int>()
的缩写。
使用透明运算符,它不仅比非透明运算符更简洁,而且还避免了一些潜在错误的发生:
cout << "Multiply: " << calculateData(cbegin(iVec), cend(iVec), 1.1, multiplies<>{}) << endl;
上例中,计算结果是double
型,如果把multiplies<>()
显式指定为int
,就会发生数据丢失的错误。
比较函数对象
当然,所有标准比较运算符也有对应的仿函数:equal_to
, not_equal_to
, less
, greater
, less_equal
和 greater_equal
。
我们可以用这些仿函数来改变priority_queue
的默认比较方式,它的模板定义如下:
template <class T, class Container = vector<T>, class Compare = less<T>>;
可以发现优先队列默认比较是less<T>
,较小元素的优先级较低,是大根堆。这里将其改变为小根堆:
priority_queue<int, vector<int>, greater<>) myQueue;
使用透明运算符
对于接受比较器类型的标准库容器来说,建议经常使用透明运算符。因为 它相对于不透明运算符可以获得更好的性能,例如下面要用字符串字面量查询set<string>
里的内容:
set<string> mySet; // set<string, less<string>
mySet.find("Key"); // 字面量"key"被拷贝构造成一个string, 浪费空间
//mySet.find("Key"sv); // 无法通过编译
set<string, less<>> mySet;
mySet.find("Key"); // 直接使用字面量"key"
mySet.find("Key"sv); // 将字面量转换为string_view
这样可以省去拷贝构造所花费的时间和空间,这种操作被称为异构查找。
此外,C++20增加了对无序关联容器使用透明运算符的支持,例如unordered_map
等。要想对它们使用透明运算符,得先实现一个自定义哈希函数,并且有一个定义为void
的is_transparent
类型别名:
// 实现一个自定义hash函数
class Hasher
{
public:
using is_transparent = void;
size_t operator() (string_view sv) const { return hash<string_view>{}(sv); }
};
// main
unordered_set<string, Hasher, equal_to<>> mySet;
逻辑函数对象
对于三种逻辑运算,也有对应的仿函数:logical_not
, logical_and
和logical_or
位函数对象
对于四种位运算,也有对应的仿函数:bit_and
, bit_or
, bit_xor
, bit_not
可调用对象的适配器
有时候我们只想使用可调用对象的部分参数,可以通过使用适配器函数对象来纠正签名不匹配的问题。
为了代码的可读性和方便性,还是推荐用 lambda表达式 而不是仿函数。这里是仿函数章节,就用仿函数做演示了。
绑定器(binder)
基本用法
std::bind()
能将某些值绑定到可调用对象的参数上,它于<functional>
中定义。例如,函数func()
接收两个参数:
void func(int num, string_view str)
{
cout << format("func({}, {})", num, str) << endl;
}
接下来使用std::bind()
,将func()
的str
参数绑定为myString
:
string myString{ "abc" };
auto f1{ bind(func, placeholders::_1, myString) };
f1(233);
// 输出: func(233, abc)
绑定引用类型参数
如果可调用对象的参数有引用类型,则需要<functional>
定义的std::ref()
和std::cref()
来帮忙,它们用于绑定类型是非const
引用和const
引用的变量。
例如下边有个函数increment()
,平常是这样使用的:
void increment(int& num)
{
++num;
}
int main()
{
int index{ 0 };
increment(index); // index = 1
}
如果要用std::bind()
,则需要std::ref()
来绑定引用类型参数:
auto f1{ bind(increment, index) };
f1(); // index = 0
auto f2{ bind(increment, ref(index)) };
f2(); // index = 1
可以发现,不用std::ref()
的话,std::bind()
中传入的是index
的 拷贝。
绑定重载函数
要使用std::bind()
绑定重载函数,需要 显式指定要绑定哪个重载函数:
void overload(int i) { cout << "int: " << i << endl; }
void overload(float f) { cout << "float: " << f << endl; }
int main()
{
auto overloadFloat{ bind(static_cast<void(*)(float)>(overload), placeholders::_1)};
overloadFloat(1.1);
}
绑定成员函数
类的每个方法都有隐式的第一个参数,它是一个指向对象实例的指针,并且可以在方法体中以this访问。因此我们还需 绑定这个隐式的第一个参数:
class Test
{
public:
void test(int p1, int p2)
{
cout << p1 << " " << p2;
}
};
int main()
{
Test myTest;
auto useTest{ bind(&Test::test, &myTest, placeholders::_1, placeholders::_2) };
useTest(1, 2);
}
否定器(negator)
not_fn()
是所谓的否定器,它是可调用对象结果的补充(补集)。Microsoft Learn的例子如下:
参考资料
- 飘零的落花 - 现代C++详解
- C++20高级编程
- 函数 | Microsoft Learn