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_equalgreater_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等。要想对它们使用透明运算符,得先实现一个自定义哈希函数,并且有一个定义为voidis_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_andlogical_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