C++not_fn逻辑取反函数适配器
C++里那个“不干正事”的not_fn:逻辑取反的温柔叛逆
你有没有试过写一个std::find_if,想找所有不是偶数的元素,结果发现得绕一圈写个lambda?或者封装了一个判断函数is_valid(),临时想用它的反面——却得再写个!is_valid(x)塞进算法里?这时候,not_fn就像悄悄递来一把小剪刀:不声不响,咔嚓一下,把逻辑翻个面。
not_fn是C++17引入的函数适配器,本质是std::not_fn(F&& f)返回一个可调用对象,它自动对原函数的返回值执行逻辑非(!)。听起来简单?但它的设计意图和实际用法,藏着不少容易被忽略的细节。
最常被误解的一点:not_fn不是给std::not1续命的替代品——它压根没继承老式函数对象那一套约束。std::not1要求被包装的函数对象必须继承自std::unary_function,还只支持一元调用;而not_fn完全无视这些陈规,天生支持任意参数个数、完美转发、SFINAE友好。它不检查你传的是什么,只管调用完再加个!。这种“懒但靠谱”的风格,反而让它在现代C++中更自然。
举个实在的例子。假设你有这样一个判断函数:
auto is_positive = [](int x) { return x > 0; };
你想找第一个非正数,传统写法可能是:
auto it = std::find_if(v.begin(), v.end(), [](int x) { return !is_positive(x); });
冗余且割裂——逻辑本是一体,却被拆成两层lambda。用not_fn就清爽得多:
auto it = std::find_if(v.begin(), v.end(), std::not_fn(is_positive));
注意:这里is_positive是闭包类型,not_fn能直接接住,无需std::ref或额外包装。它内部用的是完美转发+decltype(auto)返回,连返回类型都懒得猜,直接让编译器推导。
再深一层:not_fn真正好用的地方,其实是组合嵌套。比如你有个复合判断:
auto is_admin_and_active = [](const User& u) {
return u.role == "admin" && u.status == Status::ACTIVE;
};
现在要筛出“非管理员或已停用”的用户——也就是!(A && B),按德摩根定律等价于!A || !B。但你根本不用手动展开。直接:
auto bad_candidates = std::partition(v.begin(), v.end(), std::not_fn(is_admin_and_active));
干净利落。这里没有宏、没有模板特化、没有手写仿函数类——只有一次包装,一次否定,一次语义直译。
还有个容易踩的坑:not_fn只否定返回值,不干涉副作用。如果被包装的函数本身会修改状态(比如日志打印、计数器自增),not_fn不会阻止它执行,只是把true变成false、false变成true。这点必须心里有数——它改的是“结论”,不是“行为”。
另外,not_fn对返回类型的要求很宽松:只要!expr合法就行。这意味着它不仅能处理bool,还能处理重载了operator!的自定义类型(比如某些状态类、可空类型)。如果你的Result<T>有operator!()表示“失败”,std::not_fn(process)就能自然表达“处理未失败”——这种语义延展性,是硬写lambda很难兼顾的。
最后说个实用技巧:not_fn和std::bind或std::mem_fn搭配时特别顺手。比如你想过滤掉所有obj.is_expired()为true的对象:
std::remove_if(container.begin(), container.end(),
std::not_fn(std::mem_fn(&Obj::is_expired)));
比写[&](const auto& o) { return !o.is_expired(); }少敲十来个字符,更重要的是——意图零损耗。读代码的人一眼看懂:“留下的,是没过期的”。
当然,它也不是万能胶。如果你需要否定后做其他变换(比如-f(x)或f(x) == 0),not_fn就无能为力了;它只做!f(args...)这一件事,不多不少。这种克制,恰恰是它可靠的原因。
回到开头那个问题:为什么C++17要专门加这么个“只干一件事”的工具?因为真实编码中,我们频繁需要“取反语义”,但又不愿为每次取反都新建一个lambda、多一层缩进、多一个命名负担。not_fn不是炫技,它是对日常微小摩擦的一次精准润滑——轻、准、不抢戏。
下次当你又在lambda里打!的时候,不妨停半秒:这个!,真的需要亲手敲吗?


还没有评论,来说两句吧...