通用否定器 - std::not_fn

1. std::not_fn 概述

C++17 之前,标准库提供了 std::not1std::not2,用于对谓词(返回 bool 的函数对象)进行取反。如果你用过它们,你一定知道它们有多难用:

  1. 要求繁琐:必须给函数对象定义 argument_typeresult_type 等嵌套类型(或者继承 std::unary_function,后者在 C++11 就被废弃了)。
  2. 仅限一元和二元:如果你有一个接受 3 个参数的谓词想取反,std::not1std::not2 爱莫能助。
  3. 无法处理 Lambda:Lambda 没有那些嵌套类型,所以不能直接传给 std::not1,必须用 std::function 包裹,这会有性能损耗。

C++17 彻底解决了这个问题,推出了 std::not_fn

  • 它是谁:一个函数适配器
  • 它能干什么:接受任何可调用对象,返回一个新的可调用对象。这个新对象的逻辑是:调用原对象,并对返回值进行逻辑非!操作
  • 通用性:无论是一元、二元还是 N 元谓词;无论是函数指针、Lambda 还是成员函数,统统兼容

核心语法与头文件

  • 头文件:<functional>

  • 原型:

    1
    2
    template <class F>
    constexpr /* 未指定 */ not_fn(F&& f);

    在 C++ 标准文档中,看到未指定这三个字,它的准确意思是:标准只规定了它的功能和行为,但没有规定它具体是什么类型。

  • 返回值:一个未指定类型的可调用对象(通常是一个封装了 f 的结构体实例)。

std::not_fn 的实现原理非常优雅,它利用了 C++17 引入的 std::invoke。假设我们调用:

1
2
auto my_not_pred = std::not_fn(some_function); // some_function 是任意可调用对象
auto result = my_not_pred(arg1, arg2); // 调用生成的对象

编译器生成的 my_not_pred 对象(让我们暂且叫它 not_fn_wrapper)内部逻辑类似于伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 编译器生成的伪装类(伪代码)
template <typename Callable>
class not_fn_wrapper
{
Callable func; // 内部持有原始的可调用对象

public:
// 构造函数
not_fn_wrapper(Callable&& f) : func(std::move(f)) {}

// 核心:调用操作符 (operator())
// Args... 代表用户传入的所有参数,例如
template <typename... Args>
decltype(auto) operator()(Args&&... args) const
{

// 第一步:完美转发参数并使用 std::invoke 调用原始函数
// 这里的关键是使用了 std::invoke
bool original_result = std::invoke(func, std::forward<Args>(args)...);

// 第二步:执行逻辑取反
return !original_result;
}
};

2. std::not_fn 应用

2.1 处理 Lambda

处理 Lambda 这是最常用的场景。在算法中过滤数据时,写条件是很常见的。示例代码需求:删除容器中所有大于等于 20 的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // std::not_fn

int main()
{
std::vector<int> nums = {5, 10, 15, 20, 25, 30};

// 定义一个简单的判断条件
auto is_less_than_20 = [](int x) { return x < 20; };

// 使用 std::not_fn 包装它,变为 "大于等于 20"
auto new_end = std::remove_if(nums.begin(), nums.end(), std::not_fn(is_less_than_20));

nums.erase(new_end, nums.end());

// 输出: 5, 10, 15
for (int n : nums)
{
std::cout << n << " ";
}
std::cout << std::endl;

return 0;
}

std::remove_if 是 C++ 标准库 <algorithm> 头文件中的一个非常重要的算法。函数原型如下:

1
2
template< class ForwardIt, class UnaryPredicate >
ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p );
  • first, last: 定义要操作的范围(左闭右开)。
  • p: 一元谓词。接受一个元素,返回 bool。如果返回 true,表示该元素应当被移除。
  • 返回值: 指向新逻辑结尾的迭代器(即最后一个保留元素之后的位置)。

它的具体行为是:

  1. 遍历:它遍历容器(或者范围)内的所有元素。
  2. 判断:对每个元素应用谓词(即提供的条件函数)。
    • 如果元素不满足条件(即要保留),它会把该元素移到范围的前部。
    • 如果元素满足条件(即要移除),它就用后面保留的元素覆盖当前位置(通过移动赋值)。
  3. 返回:它返回一个迭代器,指向新的逻辑结尾之后的位置。

std::remove_if是一个不稳定的重排操作。它把保留的元素挤在一起放在容器的头部,剩下的尾部元素是未定义的(可能是原来的值,也可能是移动后的残留)。

std::remove_if 并不会真正删除容器中的元素,也不会改变容器的大小。真正的删除需要两步走:

  1. 调用 std::remove_if 把不要的元素挤到后面,获取新的结尾迭代器。
  2. 调用容器的 erase 方法,从那个新结尾一直删到 end()

2.2 多参数取反

std::not_fn 不在乎参数个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <vector>
#include <functional>
#include <numeric>

// 定义一个复杂的谓词,接收两个参数
// 只有当 a 能被 b 整除时返回 true
bool is_divisible(int a, int b)
{
return (a % b == 0);
}

int main()
{
std::vector<std::pair<int, int>> pairs = {
{10, 2}, {10, 3}, {20, 5}, {20, 6}
};

// 需求:保留那些 不可整除 的 pair
// 我们直接用 std::not_fn 包装 is_divisible
auto is_not_divisible = std::not_fn(is_divisible);

std::cout << std::boolalpha;
for (const auto& p : pairs)
{
bool result = is_not_divisible(p.first, p.second);
std::cout << p.first << " % " << p.second
<< " != 0 ? : " << result << std::endl;
}

return 0;
}

2.3 与成员函数结合

std::not_fn 内部使用 std::invoke,所以它对成员函数的支持非常自然。示例代码需求:找出所有非活跃用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <vector>
#include <functional>
#include <string>

class User
{
public:
std::string name;
bool active;

User(std::string n, bool a) : name(n), active(a) {}

bool is_active() const
{
return active;
}
};

int main()
{
std::vector<User> users = {
{"Alice", true},
{"Bob", false},
{"Charlie", true}
};

auto is_active_ptr = &User::is_active;
auto is_not_active = std::not_fn(is_active_ptr);

std::cout << "Inactive users:" << std::endl;
for (const auto& u : users)
{
if (is_not_active(u))
{
std::cout << "- " << u.name << std::endl;
}
}

return 0;
}

std::not_fn 本身不负责绑定对象,它只负责反转调用结果,但如果我们直接传 member functionstd::not_fn 返回的对象在调用时需要对象作为参数。这里有两种处理方式:

  1. 配合 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);
  2. 通过 Lambda 进行处理,不使用 std::not_fn绑定发生在定义时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    User 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>

struct ThresholdFilter
{
int limit;
ThresholdFilter(int l) : limit(l) {}

// 为了方便直接调用,重载 ()
bool operator()(int x) const
{
return x > limit;
}
};

int main()
{
ThresholdFilter my_filter(5);
auto is_not_above_ref = std::not_fn(std::ref(my_filter));

std::vector<int> nums = {4, 6, 8, 2};

std::cout << "Round 1 (Limit 5): " << std::endl;
for (int n : nums)
{
if (is_not_above_ref(n))
{
std::cout << n << " "; // 输出 <= 5 的数
}
}
std::cout << std::endl;

// --- 动态修改状态 ---
std::cout << "\nChanging limit to 10..." << std::endl;
my_filter.limit = 10;

std::cout << "Round 2 (Limit 10): " << std::endl;
for (int n : nums)
{
if (is_not_above_ref(n))
{
std::cout << n << " "; // 输出 <= 10 的数
}
}
std::cout << std::endl;

return 0;
}

std::not_fn 默认会按值(拷贝)的方式存储传入的可调用对象,如果我们直接写 std::not_fn(my_filter),它会复制一份 my_filter 存在内部,之后外界修改原始的 my_filterstd::not_fn 内部的副本不会受影响(状态隔离)。

std::ref 是一个引用包装器,用于将对象转换为 std::reference_wrapper 类型,它欺骗了 std::not_fn 的模板推导机制。虽然 std::not_fn 依然按值存储,但它存的是一个轻量级的引用对象,通过 std::not_fn(std::ref(my_filter)),包装器不再拥有独立的数据,而是指向原始的 my_filter