C++17 std::not_fn:现代谓词取反与函数适配器详解
1. 项目概述如果你在C17之前写过需要逻辑取反的谓词Predicate比如在std::find_if里找一个“不等于某个值”的元素或者用std::remove_if时想保留“不满足某个条件”的元素你大概率会和我一样怀念过其他语言里那种简洁的!操作符直接作用于函数对象的感觉。在C里我们得手动写个lambda比如[](auto x){ return !pred(x); }或者去翻那个已经有点年头的std::not1、std::not2。这些老办法要么啰嗦要么限制多用起来总感觉不够“现代”。std::not_fn就是C17标准库为了解决这个痛点而引入的新工具。它的核心目标极其明确创建一个转发调用包装器对其持有的可调用对象Callable Object的返回值进行逻辑非!操作。简单说它能把任何一个返回布尔值或可转换为布尔值的函数、函数对象、成员函数指针等变成一个逻辑上完全相反的新函数对象。这个看似简单的功能背后却体现了现代C设计哲学的演进更通用、更安全、更易于组合。它彻底取代了C03时代的std::not1和std::not2不仅统一了接口消除了对一元或二元谓词的人为区分更重要的是它完美支持了现代C的完美转发、引用限定、constexpr、noexcept传播等特性。这意味着你用它包装的谓词能几乎无损地保留原谓词的所有优良特性包括移动语义和异常规格。对于日常开发std::not_fn的价值在于极大地提升了代码的表达力和简洁性。在算法调用、条件判断、回调函数组合等场景中它能让你写出意图更清晰、更函数式的代码。接下来我们就从为什么需要它开始一步步拆解它的设计、用法和那些藏在细节里的“魔鬼”。2. 从历史痛点看std::not_fn的设计动机要真正理解std::not_fn的价值我们得先回到它出现之前的世界。在C98/03时代标准库提供了std::not1和std::not2这两个函数模板来创建谓词的否定器。2.1std::not1与std::not2的局限这两个工具用起来非常别扭。首先它们要求你使用的谓词类型必须定义特定的嵌套类型argument_type对于not1或first_argument_type和second_argument_type对于not2。这意味着只有像std::lessint、std::logical_and这样的标准库函数对象或者你自己精心按照这个规范定义的类型才能用。// C03 风格使用 std::not1 #include functional #include algorithm #include vector struct IsOdd : public std::unary_functionint, bool { // 必须继承以提供 argument_type bool operator()(int n) const { return n % 2 ! 0; } }; int main() { std::vectorint v {1, 2, 3, 4, 5}; // 使用 not1 找到第一个非奇数即偶数 auto it std::find_if(v.begin(), v.end(), std::not1(IsOdd())); // it 指向 2 }如果你用一个普通的lambda表达式C11之后或者一个没有定义这些嵌套类型的函数对象编译器会报出一堆晦涩的错误。其次not1和not2的命名就暗示了它们只能分别处理一元和二元谓词这种人为的划分在现代C的泛型编程中显得非常僵化。最后它们对现代C的右值引用、完美转发等特性支持得很差或者根本不支持。2.2 现代C的替代方案及其不足C11引入了lambda我们似乎有了一个“万能”的解决方案auto is_even [](int n){ return n % 2 0; }; // 手动取反 auto is_not_even [](int n){ return !is_even(n); };这确实能工作但它有几个问题样板代码每次都需要写一个几乎一样的lambda特别是当原谓词参数复杂或涉及完美转发时代码会变得冗长。类型膨胀每个手动写的取反lambda都是一个独立的、全新的类型尽管它们逻辑等价。这在模板元编程或需要类型推导的场合可能带来不必要的复杂度。特性丢失手动包装可能会丢失原谓词的noexcept规格、constexpr属性等。你需要手动为新的lambda添加这些说明符既麻烦又容易出错。组合困难当需要多层逻辑组合例如非A且非B时嵌套的lambda会让代码可读性急剧下降。std::not_fn的设计目标就是提供一个类型安全、泛型、能保留原可调用对象特性、并且易于使用的标准方案来一劳永逸地解决谓词取反的问题。2.3std::not_fn的核心设计思想std::not_fn的实现遵循了现代C库组件的几个核心原则值语义与泛型它接受一个任意类型的可调用对象F通过std::decay_tF获取其值类型并存储一份副本或移动后的状态。这使得它可以处理函数指针、成员指针、lambda、任何重载了operator()的类对象。完美转发它的operator()是模板化的使用Args...和std::forward来完美转发参数确保参数的值类别左值/右值和常量性得以保持。引用限定与常量性它提供了四个重载版本的operator()、const、、const。这确保了无论取反器本身是左值、右值、常量还是非常量都能正确地调用内部存储的可调用对象并支持移动语义。SFINAE友好与noexcept传播其返回类型使用decltype和std::invoke_result_tC17或直接使用std::invoke的表达式C20来推导确保只有在原谓词能以给定参数调用且结果可被!操作时not_fn的调用才是有效的。同时noexcept规格会基于内部调用是否抛出异常来推导保留了原谓词的异常安全性。constexpr支持从C20起std::not_fn本身和其operator()都是constexpr的允许在编译期求值。这种设计使得std::not_fn成为一个强大的、基础性的函数适配器也为C20引入的std::bind_front等更高级的组合工具奠定了设计模式上的基础。3.std::not_fn的接口与核心实现解析让我们深入到std::not_fn的接口定义和典型实现中看看它是如何将上述设计思想落地的。根据C标准std::not_fn有两个重载我们主要关注最常用的第一个。3.1 函数签名与模板参数template class F constexpr /* unspecified */ not_fn( F f );模板参数F这是一个转发引用F意味着它可以接受左值或右值并利用引用折叠规则保持参数的值类别。返回类型/* unspecified */标准故意不指定具体的返回类型只规定了该类型必须满足的行为即一个可调用对象调用时返回!std::invoke(fd, args...)。这给了编译器实现最大的优化自由度。通常它会返回一个类似not_fn_tstd::decay_tF的内部类模板实例。constexpr(C20起)意味着该函数可以在常量表达式中使用。3.2 返回类型的内部结构虽然具体类型未指定但其行为是严格规定的。我们将其称为包装器类型W。W内部会存储一个std::decay_tF类型的成员对象假设叫fd。构造W的构造函数是explicit的它用std::forwardF(f)直接初始化成员fd。这意味着如果你传递一个右值它会被移动到fd中如果传递左值它会被复制。explicit防止了意外的隐式转换。调用操作符operator()这是核心。标准规定了四个重载分别对应包装器对象本身的四种值类别和常量性组合。我们以左值引用限定版本为例C20后template class... Args constexpr auto operator()( Args... args ) noexcept(/* see below */) - decltype(!std::invoke(std::declvalstd::decay_tF(), std::declvalArgs()...)) { // 等价于 return !std::invoke(fd, std::forwardArgs(args)...); }完美转发参数Args... args接收任意数量和类型的参数。使用std::invoke这是关键。std::invoke是C17引入的通用调用机制它不仅能用()调用普通函数对象还能处理成员函数指针和成员数据指针。std::invoke(fd, args...)等价于如果fd是成员函数指针pmf则调用为(obj.*pmf)(args...)或(ptr-*pmf)(args...)取决于第一个参数。如果fd是成员数据指针pmd则返回obj.*pmd或ptr-*pmd。否则就是普通的fd(args...)。 这使得std::not_fn能统一处理几乎所有可调用实体。逻辑非操作对std::invoke的结果应用!操作符。这就要求std::invoke的返回类型必须是可以上下文转换为bool的类型如bool、int、指针等。返回类型推导使用尾返回类型decltype(!std::invoke(...))确保返回类型与表达式类型完全一致。noexcept规格noexcept中的条件表达式会计算!std::invoke(fd, std::forwardArgs(args)...)是否可能抛出异常。这实现了noexcept的自动传播。引用限定表示这个operator()只能在W类型的左值对象上调用。对应的const用于常量左值用于右值例如临时对象const用于常量右值。右值版本会std::move(fd)将内部存储的可调用对象作为右值传递给std::invoke这对于那些移动后状态会改变的函数对象很重要。3.3 一个简化的实现示例为了更直观地理解下面是一个高度简化、忽略了一些边缘情况如C20的constexpr、删除的重载的实现思路namespace detail { template typename F class not_fn_t { F f; // 存储的可调用对象 public: explicit not_fn_t(F func) : f(std::forwardF(func)) {} // 左值版本 template typename... Args auto operator()(Args... args) - decltype(!std::invoke(f, std::forwardArgs(args)...)) { return !std::invoke(f, std::forwardArgs(args)...); } // 常量左值版本 template typename... Args auto operator()(Args... args) const - decltype(!std::invoke(f, std::forwardArgs(args)...)) { return !std::invoke(f, std::forwardArgs(args)...); } // 右值版本 template typename... Args auto operator()(Args... args) - decltype(!std::invoke(std::move(f), std::forwardArgs(args)...)) { return !std::invoke(std::move(f), std::forwardArgs(args)...); } // 常量右值版本 template typename... Args auto operator()(Args... args) const - decltype(!std::invoke(std::move(f), std::forwardArgs(args)...)) { return !std::invoke(std::move(f), std::forwardArgs(args)...); } }; } template typename F auto not_fn(F f) { // 用 std::decay_t 去除引用和cv限定符获得值类型 return detail::not_fn_tstd::decay_tF(std::forwardF(f)); }这个简化版清晰地展示了存储、转发、调用、取反的核心流程。实际标准库的实现会更复杂需要处理SFINAE、noexcept、constexpr以及C20后为了更清晰的错误信息而引入的deleted重载。注意永远不要试图自己实现一个用于生产环境的std::not_fn。标准库的实现经过了极端严格的测试和编译器优化自己实现的版本在边缘情况如ADL、重载决议、异常规格的精确传播上很容易出错。这里展示只是为了理解原理。4. 实战演练std::not_fn的多种应用场景理解了原理我们来看看std::not_fn在实际编码中能如何大显身手。它的应用场景几乎涵盖了所有需要逻辑取反的泛型编程场合。4.1 与标准库算法协同工作这是std::not_fn最经典的应用。标准库中许多算法接受一个谓词Predicate。场景一查找“不满足”条件的元素#include algorithm #include vector #include functional #include iostream int main() { std::vectorint vec {1, 2, 3, 4, 5, 6}; // 定义一个谓词判断是否为偶数 auto is_even [](int n) { return n % 2 0; }; // 使用 std::not_fn 找到第一个奇数 auto first_odd_it std::find_if(vec.begin(), vec.end(), std::not_fn(is_even)); if (first_odd_it ! vec.end()) { std::cout 第一个奇数是: *first_odd_it \n; // 输出 1 } // 移除所有非偶数即奇数 vec.erase(std::remove_if(vec.begin(), vec.end(), std::not_fn(is_even)), vec.end()); // 现在 vec 包含 {2, 4, 6} for (int n : vec) std::cout n ; }优势代码意图非常清晰。std::not_fn(is_even)直接表达了“非偶数”这个概念比写[](int n){ return !is_even(n); }或[](int n){ return n % 2 ! 0; }更声明式也减少了重复逻辑。场景二区间划分与排序#include algorithm #include vector #include string #include functional struct Person { std::string name; int age; }; int main() { std::vectorPerson people {{Alice, 30}, {Bob, 25}, {Charlie, 35}, {Diana, 28}}; // 按年龄是否大于等于30岁进行划分 auto is_senior [](const Person p) { return p.age 30; }; auto partition_point std::partition(people.begin(), people.end(), is_senior); // people 前段是 senior (30), 后段是 junior (30) // 但如果我想把 junior 放在前面呢直接用 not_fn std::partition(people.begin(), people.end(), std::not_fn(is_senior)); // 现在 people 前段是 junior (30), 后段是 senior (30) }4.2 处理成员函数指针和成员对象指针std::invoke的强大使得std::not_fn可以直接用于成员指针这在面向对象编程中非常方便。#include functional #include vector #include algorithm #include iostream class Widget { public: bool is_ready() const { return ready_; } int get_value() const { return value_; } private: bool ready_ false; int value_ 42; }; int main() { std::vectorWidget widgets(5); // 使用 not_fn 找到第一个 is_ready() 返回 false 的 Widget auto it std::find_if(widgets.begin(), widgets.end(), std::not_fn(Widget::is_ready)); // 传递成员函数指针 // it 将指向第一个元素因为所有 Widget 的 ready_ 默认都是 false // 一个更复杂的例子结合 std::bind 或 lambda 处理带参数的成员函数 // 假设我们想比较 value_ 与一个阈值 auto is_value_above [threshold50](const Widget w) { return w.get_value() threshold; }; // 找到 value 不大于阈值的 Widget auto it2 std::find_if(widgets.begin(), widgets.end(), std::not_fn(is_value_above)); }注意直接传递Widget::is_ready给std::not_fn时std::invoke期望的第一个参数是Widget对象或指针/引用。在std::find_if中迭代器解引用得到Widget正好匹配。如果容器里是指针如vectorWidget*则需要用std::mem_fn或lambda先进行适配或者使用C20的std::bind_front。4.3 构建复杂的谓词逻辑std::not_fn可以与其他函数对象组合构建更复杂的逻辑虽然C标准库没有直接提供逻辑与/或的泛型适配器但我们可以手动组合。#include functional #include algorithm #include vector int main() { std::vectorint nums {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto is_even [](int n) { return n % 2 0; }; auto is_multiple_of_3 [](int n) { return n % 3 0; }; // 找到既不是偶数也不是3的倍数的数 (即 !(偶数 || 3的倍数)) // 手动组合 lambda auto is_neither [](int n) { return !(is_even(n) || is_multiple_of_3(n)); }; auto it1 std::find_if(nums.begin(), nums.end(), is_neither); // it1 指向 1, 5, 7... // 使用 not_fn 可以稍微清晰一些但逻辑运算仍需lambda auto is_even_or_multiple_of_3 [](int n) { return is_even(n) || is_multiple_of_3(n); }; auto it2 std::find_if(nums.begin(), nums.end(), std::not_fn(is_even_or_multiple_of_3)); // 与 it1 结果相同 }虽然对于复杂的逻辑组合最终可能还是需要lambda但std::not_fn在否定一个已经定义好的复杂谓词时依然能保持代码的清晰。C20的Ranges库和未来的提案可能会提供更强大的函数组合工具。4.4 在自定义函数和回调中的应用不限于标准库算法任何接受谓词作为参数的地方都可以使用std::not_fn。#include functional #include iostream #include vector templatetypename Iter, typename Predicate Iter find_if_not(Iter first, Iter last, Predicate pred) { // 一个简单的 find_if_not 实现使用 not_fn return std::find_if(first, last, std::not_fn(pred)); } void process_data(const std::vectorint data, std::functionbool(int) filter, std::functionvoid(int) processor) { for (int value : data) { if (filter(value)) { processor(value); } } } int main() { std::vectorint data {10, 20, 30, 40, 50}; auto is_greater_than_25 [](int x) { return x 25; }; // 使用 not_fn 作为过滤条件处理不大于25的数 process_data(data, std::not_fn(is_greater_than_25), // 过滤条件x 25 [](int x) { std::cout Processing: x \n; }); // 输出: Processing: 10, Processing: 20, Processing: 25(如果存在) }5. 深入细节std::not_fn的注意事项与陷阱即使是一个设计良好的工具也有其特定的使用边界和需要注意的细节。在实际项目中踩过几次坑后我总结了一些关键点。5.1 对可调用对象的要求std::not_fn的核心操作是对std::invoke的结果应用!运算符。这隐含了两个必须满足的条件调用必须有效对于给定的参数集std::invoke(fd, args...)必须是一个合法的表达式。这意味着参数的数量和类型必须与存储的可调用对象fd匹配。结果必须可布尔取反std::invoke(fd, args...)的返回类型必须支持!运算符或者其结果可以上下文转换为bool。基本类型如bool、int、指针等都没问题。但如果返回一个用户自定义类型并且没有重载operator!或定义到bool的转换那么编译就会失败。struct MyFunctor { std::string operator()(int) const { return hello; } }; int main() { MyFunctor f; auto negated std::not_fn(f); // auto result negated(42); // 错误 // 对 std::string 类型的返回值应用 ! 运算符是未定义的。 }5.2 关于值类别与移动语义的微妙之处std::not_fn返回的包装器对象内部存储了原始可调用对象的副本或移动后的状态。这里有一个重要的生命周期问题需要警惕。#include functional #include iostream auto create_predicate() { int threshold 10; // 返回一个捕获了局部变量 threshold 的 lambda return [threshold](int x) { return x threshold; }; } int main() { // 危险lambda 捕获的 threshold 是 create_predicate 中局部变量的副本。 // 但 lambda 本身是安全的因为它持有数据的副本。 auto pred create_predicate(); // 使用 not_fn 包装这个 lambda auto not_pred std::not_fn(pred); // 这里复制了 lambdalambda 内部又复制了 threshold没问题。 std::cout not_pred(5) \n; // 输出 1 (true, 因为 5 10 为 false取反为 true) // 但是如果 lambda 通过引用捕获呢 int local_threshold 20; auto ref_pred [local_threshold](int x) { return x local_threshold; }; auto not_ref_pred std::not_fn(ref_pred); // 复制了 lambda但 lambda 内部是对 local_threshold 的引用 local_threshold 5; // 修改被引用的变量 std::cout not_ref_pred(10) \n; // 输出 0 (false, 因为 10 5 为 true取反为 false) // 虽然能运行但逻辑依赖于外部变量可能造成困惑。如果 local_threshold 离开作用域则引用悬空行为未定义。 }关键点std::not_fn复制/移动的是可调用对象本身而不是它可能捕获的外部状态。如果可调用对象如lambda通过引用捕获了局部变量那么即使std::not_fn包装器被安全地传递其内部逻辑仍然依赖于那个可能已经失效的引用。对于通过值捕获的lambda则是安全的。5.3noexcept规格的保留从C17开始noexcept成为了类型系统的一部分。std::not_fn会尽力保留底层调用的noexcept属性。这对于编写异常安全的泛型代码很重要。#include functional #include iostream auto throwing_lambda [](int) noexcept(false) { /* 可能抛出 */ return true; }; auto non_throwing_lambda [](int) noexcept { return false; }; int main() { auto nt1 std::not_fn(throwing_lambda); auto nt2 std::not_fn(non_throwing_lambda); // 在C17/20中noexcept 运算符会反映内部调用的异常规格 std::cout std::boolalpha; std::cout nt1 is noexcept? noexcept(nt1(0)) \n; // 可能输出 false std::cout nt2 is noexcept? noexcept(nt2(0)) \n; // 应该输出 true }在泛型代码中你可以利用noexcept(expr)来根据谓词的异常安全性进行条件编译或优化。5.4 与std::bind、std::bind_front的交互std::not_fn经常与std::bind或C20的std::bind_front一起使用以创建部分应用的谓词。#include functional #include algorithm #include vector #include string bool is_substring(const std::string str, const std::string substr) { return str.find(substr) ! std::string::npos; } int main() { std::vectorstd::string words {hello, world, hello world, foo}; // 使用 std::bind 将第二个参数绑定为 hello using namespace std::placeholders; auto contains_hello std::bind(is_substring, _1, hello); // 使用 not_fn 找到不包含 hello 的字符串 auto it std::find_if(words.begin(), words.end(), std::not_fn(contains_hello)); // it 指向 world 或 foo // C20 更推荐使用 std::bind_front语法更清晰 auto contains_hello_front std::bind_front(is_substring, hello); auto it2 std::find_if(words.begin(), words.end(), std::not_fn(contains_hello_front)); }注意std::bind返回的对象类型是未指定的并且可能比较重存储绑定参数和可调用对象的副本。std::not_fn包装它没有问题但要注意性能开销。std::bind_frontC20通常是更好的选择它更轻量且意图更明确。5.5 C26 的新重载静态可调用对象根据你提供的cppreference资料C26为std::not_fn引入了一个新的重载允许传递一个静态确定的可调用对象作为非类型模板参数。template auto ConstFn constexpr /* unspecified */ not_fn() noexcept;这个版本用于当你有一个编译期已知的可调用实体如函数指针、静态成员函数指针、无捕获的lambda转换成的函数指针、或者inline constexpr的函数对象并且你想得到一个无状态的、编译期可构造的取反器。// 假设有这样一个函数 constexpr bool is_positive(int x) noexcept { return x 0; } int main() { #if __cpp_lib_not_fn 202306L // C26 特性测试宏 // 使用模板参数版本 auto not_positive std::not_fnis_positive(); static_assert(not_positive(-5) true); // 编译期求值 static_assert(noexcept(not_positive(0))); // 异常规格保留 // 对于无捕获的lambda可以转换为函数指针使用 auto is_even_lambda [](int n) constexpr noexcept - bool { return n % 2 0; }; // 注意需要获取lambda的operator()指针通常通过lambda或赋值给函数指针变量 constexpr bool (*is_even_ptr)(int) [](int n) noexcept { return n % 2 0; }; auto not_even std::not_fnis_even_ptr(); #endif }这个版本的not_fn返回的是一个无状态的调用包装器它不存储任何数据只包含对模板参数ConstFn的调用。这可以带来潜在的优化机会例如编译器可能完全内联掉调用并且保证了该对象是默认可构造的、可赋值的如果底层类型允许。对于高性能或对对象大小敏感的场景这是一个有价值的补充。6. 性能考量与最佳实践在性能关键的代码中任何抽象都可能带来开销。std::not_fn的设计目标之一是零开销抽象但在实际中我们需要理解其成本并明智地使用。6.1 开销分析构造开销std::not_fn(f)会构造一个内部包装器对象该对象会存储std::decay_tF类型的一个副本。这意味着会发生一次F类型的复制或移动构造。如果F是一个很大的函数对象例如捕获了很多数据的lambda这个复制开销可能不可忽视。对于轻量级对象如函数指针、无捕获lambda、小型的自定义函数对象这个开销通常可以忽略。调用开销operator()的内部实现基本上就是一次std::invoke调用加上一个!操作。在开启了优化的编译器中对于简单的可调用对象这些调用很可能被完全内联达到与手动写!f(args...)相同的机器码。引用限定的多个重载在编译期就能确定不会带来运行时分支。间接调用如果存储的可调用对象本身是一个运行时多态的对象比如std::function那么std::not_fn的调用会多一层转发但主要的开销来自于std::function本身的虚调用或函数指针调用not_fn增加的只是一个简单的取反操作开销极小。6.2 最佳实践建议优先用于轻量级谓词对于函数指针、无捕获lambda、小的结构体可以放心使用std::not_fn。对于捕获了大量数据或持有资源的lambda考虑是否真的需要复制整个谓词或者能否通过引用捕获共享数据但要注意生命周期。考虑内联lambda在非常简单的一次性使用场景中直接写一个取反的lambda[](auto... args) { return !pred(args...); }可能和std::not_fn(pred)一样清晰并且可能让编译器更容易优化因为它是一个全新的、独立的类型。但std::not_fn在需要将取反谓词作为参数传递或存储时提供了更好的类型抽象和一致性。与std::function配合使用需谨慎std::function本身就有类型擦除的开销。std::not_fn(std::function)会先复制std::function然后调用时多一层转发。如果性能敏感可以考虑直接定义一个包含取反逻辑的新std::function或者使用模板来避免类型擦除。利用constexpr(C20)如果谓词和参数都是编译期常量使用constexpr的std::not_fn可以在编译期完成计算消除所有运行时开销。清晰性优于微优化在大多数非极端性能敏感的代码中std::not_fn带来的代码清晰性和可维护性提升远大于其可能带来的微小性能损失。优先写出意图明确的代码。6.3 一个简单的性能对比示例仅供参考#include functional #include chrono #include iostream #include vector #include algorithm #include random void benchmark() { constexpr size_t size 1000000; std::vectorint data(size); std::mt19937 gen(42); std::uniform_int_distribution dis(0, 100); std::generate(data.begin(), data.end(), [](){ return dis(gen); }); auto is_even [](int n) { return n % 2 0; }; // 方法1手动lambda取反 auto start1 std::chrono::high_resolution_clock::now(); auto cnt1 std::count_if(data.begin(), data.end(), [](int n) { return !is_even(n); }); auto end1 std::chrono::high_resolution_clock::now(); // 方法2使用 std::not_fn auto start2 std::chrono::high_resolution_clock::now(); auto cnt2 std::count_if(data.begin(), data.end(), std::not_fn(is_even)); auto end2 std::chrono::high_resolution_clock::now(); auto duration1 std::chrono::duration_caststd::chrono::microseconds(end1 - start1); auto duration2 std::chrono::duration_caststd::chrono::microseconds(end2 - start2); std::cout Manual lambda: duration1.count() us, count cnt1 \n; std::cout std::not_fn: duration2.count() us, count cnt2 \n; // 在 -O2 或 -O3 优化下两者时间通常几乎相同。 }在我的测试环境GCC 13.2 -O3下两者的性能差异在测量误差范围内。这印证了现代编译器优化能力的强大。7. 迁移指南从旧式not1/not2升级到std::not_fn如果你的代码库中还有使用std::not1或std::not2的遗留代码迁移到std::not_fn通常是直截了当的并且能带来更好的安全性和通用性。7.1 直接替换大多数情况下你可以直接将std::not1(pred)或std::not2(pred)替换为std::not_fn(pred)。迁移前 (C11/14但使用老式适配器)#include functional #include algorithm #include vector struct GreaterThanFive : public std::unary_functionint, bool { bool operator()(int x) const { return x 5; } }; int main() { std::vectorint v {1, 8, 3, 10, 5}; // 使用 not1 auto it std::find_if(v.begin(), v.end(), std::not1(GreaterThanFive())); }迁移后 (C17)#include functional #include algorithm #include vector // 不再需要继承 std::unary_function struct GreaterThanFive { bool operator()(int x) const { return x 5; } }; int main() { std::vectorint v {1, 8, 3, 10, 5}; // 直接使用 not_fn auto it std::find_if(v.begin(), v.end(), std::not_fn(GreaterThanFive())); // 或者更现代地使用lambda auto it2 std::find_if(v.begin(), v.end(), std::not_fn([](int x){ return x 5; })); }7.2 处理二元谓词std::not2用于二元谓词std::not_fn可以统一处理。迁移前#include functional #include algorithm #include vector #include string bool case_insensitive_compare(const std::string a, const std::string b) { // 忽略大小写比较的实现 return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char c1, char c2) { return std::tolower(c1) std::tolower(c2); }); } int main() { std::vectorstd::string words {Apple, banana, cherry}; std::sort(words.begin(), words.end(), std::not2(std::ptr_fun(case_insensitive_compare))); }迁移后#include functional #include algorithm #include vector #include string bool case_insensitive_compare(const std::string a, const std::string b) { // 忽略大小写比较的实现 return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char c1, char c2) { return std::tolower(c1) std::tolower(c2); }); } int main() { std::vectorstd::string words {Apple, banana, cherry}; // 使用 not_fn不再需要 ptr_fun std::sort(words.begin(), words.end(), std::not_fn(case_insensitive_compare)); // 注意这会将比较结果取反可能不是你想要的行为。 // 更常见的可能是定义一个反向比较器或者使用 std::greater{}。 // 这里只是演示语法替换。 }重要提示std::not2和std::not_fn在用于std::sort这样的二元比较器时逻辑是“对比较结果取反”。这通常不是获取逆序排序的正确方式。std::sort的默认比较是std::less对其取反得到!(a b)这不等价于(a b)因为可能有相等的情况。获取逆序排序应直接使用std::greater{}或自定义一个返回a b的比较器。not_fn在这里的演示仅用于语法迁移。7.3 编译标志与兼容性C标准确保你的编译器支持C17或更高版本并设置了相应的编译标志如-stdc17,-stdc20。头文件std::not_fn定义在functional头文件中。弃用警告在C17中std::not1和std::not2被标记为deprecated在C20中被移除。使用新代码时编译器可能会对使用旧适配器的代码发出警告。使用std::not_fn可以消除这些警告。特性测试宏你可以使用__cpp_lib_not_fn宏来检测编译器是否支持std::not_fn。其值201603L表示支持C17版本202306L表示支持C26的静态版本。#if __cpp_lib_not_fn 201603L // 可以使用 std::not_fn auto negated std::not_fn(my_predicate); #else // 回退方案使用手动lambda或条件编译 auto negated [](auto... args) { return !my_predicate(std::forwarddecltype(args)(args)...); }; #endif迁移到std::not_fn不仅是语法上的更新更是向更安全、更泛型的现代C编程风格靠拢。它消除了对特定嵌套类型的依赖使得你的谓词定义更加自由代码也更加健壮。