通用否定器 - std::not_fn

通用否定器 - std::not_fn
苏丙榅1. std::not_fn 概述
在 C++17 之前,标准库提供了 std::not1 和 std::not2,用于对谓词(返回 bool 的函数对象)进行取反。如果你用过它们,你一定知道它们有多难用:
- 要求繁琐:必须给函数对象定义
argument_type和result_type等嵌套类型(或者继承std::unary_function,后者在 C++11 就被废弃了)。 - 仅限一元和二元:如果你有一个接受 3 个参数的谓词想取反,
std::not1和std::not2爱莫能助。 - 无法处理 Lambda:Lambda 没有那些嵌套类型,所以不能直接传给
std::not1,必须用std::function包裹,这会有性能损耗。
C++17 彻底解决了这个问题,推出了 std::not_fn。
- 它是谁:一个函数适配器。
- 它能干什么:接受任何可调用对象,返回一个新的可调用对象。这个新对象的逻辑是:调用原对象,并对返回值进行逻辑非(
!)操作。 - 通用性:无论是一元、二元还是 N 元谓词;无论是函数指针、Lambda 还是成员函数,统统兼容
核心语法与头文件:
头文件:
<functional>原型:
1
2template <class F>
constexpr /* 未指定 */ not_fn(F&& f);在 C++ 标准文档中,看到未指定这三个字,它的准确意思是:标准只规定了它的功能和行为,但没有规定它具体是什么类型。
返回值:一个未指定类型的可调用对象(通常是一个封装了
f的结构体实例)。
std::not_fn 的实现原理非常优雅,它利用了 C++17 引入的 std::invoke。假设我们调用:
1 | auto my_not_pred = std::not_fn(some_function); // some_function 是任意可调用对象 |
编译器生成的 my_not_pred 对象(让我们暂且叫它 not_fn_wrapper)内部逻辑类似于伪代码:
1 | // 编译器生成的伪装类(伪代码) |
2. std::not_fn 应用
2.1 处理 Lambda
处理 Lambda 这是最常用的场景。在算法中过滤数据时,写非条件是很常见的。示例代码需求:删除容器中所有大于等于 20 的元素。
1 |
|
std::remove_if 是 C++ 标准库 <algorithm> 头文件中的一个非常重要的算法。函数原型如下:
1 | template< class ForwardIt, class UnaryPredicate > |
- first, last: 定义要操作的范围(左闭右开)。
- p: 一元谓词。接受一个元素,返回
bool。如果返回true,表示该元素应当被移除。 - 返回值: 指向新逻辑结尾的迭代器(即最后一个保留元素之后的位置)。
它的具体行为是:
- 遍历:它遍历容器(或者范围)内的所有元素。
- 判断:对每个元素应用谓词(即提供的条件函数)。
- 如果元素不满足条件(即要保留),它会把该元素移到范围的前部。
- 如果元素满足条件(即要移除),它就用后面保留的元素覆盖当前位置(通过移动赋值)。
- 返回:它返回一个迭代器,指向新的逻辑结尾之后的位置。
std::remove_if是一个不稳定的重排操作。它把保留的元素挤在一起放在容器的头部,剩下的尾部元素是未定义的(可能是原来的值,也可能是移动后的残留)。
std::remove_if 并不会真正删除容器中的元素,也不会改变容器的大小。真正的删除需要两步走:
- 调用
std::remove_if把不要的元素挤到后面,获取新的结尾迭代器。 - 调用容器的
erase方法,从那个新结尾一直删到end()。
2.2 多参数取反
std::not_fn 不在乎参数个数。
1 |
|
2.3 与成员函数结合
std::not_fn 内部使用 std::invoke,所以它对成员函数的支持非常自然。示例代码需求:找出所有非活跃用户。
1 |
|
std::not_fn 本身不负责绑定对象,它只负责反转调用结果,但如果我们直接传 member function,std::not_fn 返回的对象在调用时需要对象作为参数。这里有两种处理方式:
配合
std::invoke规则:绑定发生在调用时。1
2
3
4
5
6
7
8
9
10// 1. 定义成员函数指针(它是残缺的,缺对象)
auto func_ptr = &User::is_active;
// 2. std::not_fn 只管包装,不管补对象
// is_not_active 现在等价于:! (接收到的对象.is_active)
auto is_not_active = std::not_fn(func_ptr);
// 3. 调用时刻:必须手动给它对象 'u'
// std::not_fn 内部会利用 invoke 规则,自动把 'u' 和 'func_ptr' 组合起来
is_not_active(u);通过
Lambda进行处理,不使用std::not_fn:绑定发生在定义时。1
2
3
4
5
6
7
8
9
10User bob("Bob", false);
// 这里用 lambda 捕获了 bob(这就叫“绑定对象”)
// 这里我们手动取反 !bob.is_active()
auto is_not_active_bob = [&bob]() {
return !bob.is_active();
};
// 调用时,不需要再传对象了
is_not_active_bob();
2.4 引用与拷贝的细节
在使用 std::not_fn 时,最大的坑在于它内部到底存了什么?std::not_fn 直接存储传入的可调用对象 f。
如果
f是左值引用,它会将引用移除,存储一个拷贝(或移动)。这意味着,如果你有一个巨大的仿函数对象,或者你的仿函数包含状态,你需要注意std::not_fn创建的是一份新的副本。如果你需要
std::not_fn包装的对象引用原始数据,你需要显式地传引用类型,或者在 C++23 之前通过std::ref包装。
下面这段代码通过一个具体的动态过滤器案例,演示了关于函数包装器、对象拷贝与引用传递的高级用法。
1 |
|
std::not_fn 默认会按值(拷贝)的方式存储传入的可调用对象,如果我们直接写 std::not_fn(my_filter),它会复制一份 my_filter 存在内部,之后外界修改原始的 my_filter,std::not_fn 内部的副本不会受影响(状态隔离)。
std::ref 是一个引用包装器,用于将对象转换为 std::reference_wrapper 类型,它欺骗了 std::not_fn 的模板推导机制。虽然 std::not_fn 依然按值存储,但它存的是一个轻量级的引用对象,通过 std::not_fn(std::ref(my_filter)),包装器不再拥有独立的数据,而是指向原始的 my_filter。

















