一、std::not_fn定义和说明
std::not_fn这个模板函数非常有意思,在前面我们学习过wrapper(包装器),其实它就是通过封装一个包装器来实现返回值的非。它的基本定义如下:
c
template< class F >
/* 未指定 */ not_fn( F&& f );(1) (C++17 起)(C++20 起为 constexpr)
template< auto ConstFn >
constexpr /* 未指定 */ not_fn() noexcept;(2) (C++26 起)
这里面的f参数是一个Callable对象。它的受限条件为:
1、std::decay_t 须为可调用 (Callable) 并支持移动构造 (MoveConstructible)
2、std::is_constructible_v<std::decay_t, F> 的结果必须为 true
那么,问题就来了,为什么要搞这么简单的一个东西呢?直接操作不更简单么?
二、应用
老生再次常谈一下,一切的技术的应用,跟场景的结合是无法独立出来的。也就是说,std::not_fn的应用,也不是放置四海皆优秀的。在实际的开发中,可能会遇到很多种情况,比如普通的函数,类成员函数,仿函数,甚至新标准中的Lambda表达式等等。
在处理这些情况的时候儿,可能直接操作返回一个非的结果很简单,也可能比较不简单。更有可能虽然简单但不好理解。而有的情况下开发者需要的不是一个简单的结果而是一个非的函数,凡此种种,都可能会有不同的需要。
一般来说这种在上层进行封装的应用,大多数情况下在底层应用比较多,比如本身就是库或框架。一如前面看到的STL中的元函数(如std::is_integral等),在业务层展现应用的机会很少,但一旦到底层的库编程,则应用大行其道。
三、混合应用
开发者经常会遇到这种问题,实现一个问题的一面(正面或反面)比较简单,而实现另外一面则相对复杂一些。这种简单和复杂不单指的实现上的简单,也包括代码阅读上的简单。比如实现一个返回True,需要一行代码,而返回False则需要几行代码。这种情况下,直接修改函数本身是没有问题的,但应用起来就等于是多实现了一次,而且如果需要将函数本身做为参数传递的话,这又是一个问题,很有可能在嵌套传参时导致行为的变形。
这时,使用std::not_fn直接生成一个新的非的函数,应用起来就会非常清晰明了。下面看一个简单的例程:
c
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
// 是否为偶数
bool is_even(int x) {
return x % 2 == 0;
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto is_odd = std::not_fn(is_even);
// 打印
std::vector<int> odd_numbers;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(odd_numbers), is_odd);
std::cout << "Odd numbers: ";
for (int num : odd_numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
其实在基础库的很类似的应用 中都可以应用到std::not_fn,在代码层级更容易理解。
四、特点
std::not_fn主要是为了替代前面的替代std::not1,std::not2这类非相关的处理。毕竟后两个依赖于std::unary_function 或 std::binary_function的实现。从设计角度看,这种应用方式首先就限定的灵活性及通用性或者理解为适配性不好。那么std::not_fn有什么特点呢:
1、优势
a)更高一层的抽象可以让开发者离开代码内部的实现,专注于逻辑的实现
b)适应性强,对所有的类函数(如普通函数、类内部成员函数、仿函数等等)都支持
c)c++20后支持constexpr,可在编译期进行优化
d)代码更直观,容易理解和维护
2、劣势
a)过深的嵌套可能逻辑的复杂化和提高后期维护的复杂度
b)应用范围受限,一般要求返回值的类型必须为可转化为bool类型的数据类型
c)在很简单的非应用时,std::not_fn并没有优势
所以开发者的头脑中始终要有根弦警惕着,技术要善用而非乱用!
五、例程
下面看一个cppreference的例程:
c
#include <cassert>
#include <functional>
bool is_same(int a, int b) noexcept
{
return a == b;
}
struct S
{
int val;
bool is_same(int arg) const noexcept { return val == arg; }
};
int main()
{
// 用于自由函数:
auto is_differ = std::not_fn(is_same);
assert(is_differ(8, 8) == false); // 等价于:!is_same(8, 8) == false
assert(is_differ(6, 9) == true); // 等价于:!is_same(8, 0) == true
// 用于成员函数:
auto member_differ = std::not_fn(&S::is_same);
assert(member_differ(S{3}, 3) == false); // 等价于:S tmp{6}; !tmp.is_same(6) == false
// 保持 noexcept 说明:
static_assert(noexcept(is_differ) == noexcept(is_same));
static_assert(noexcept(member_differ) == noexcept(&S::is_same));
// 用于函数对象:
auto same = [](int a, int b) { return a == b; };
auto differ = std::not_fn(same);
assert(differ(1, 2) == true); // 等价于:!same(1, 2) == true
assert(differ(2, 2) == false); // 等价于:!same(2, 2) == false
#if __cpp_lib_not_fn >= 202306L
auto is_differ_cpp26 = std::not_fn<is_same>();
assert(is_differ_cpp26(8, 8) == false);
assert(is_differ_cpp26(6, 9) == true);
auto member_differ_cpp26 = std::not_fn<&S::is_same>();
assert(member_differ_cpp26(S{3}, 3) == false);
auto differ_cpp26 = std::not_fn<same>();
static_assert(differ_cpp26(1, 2) == true);
static_assert(differ_cpp26(2, 2) == false);
#endif
}
例程非常简单,其实可以理解为对函数指针F的一种非的反向控制。它可以提供更灵活的控制方式,而不必直接修改相关的代码。
六、总结
std::not_fn本身并没有什么难度。但只要认真想一下,其实就难明白,它其实就是让编程变得更灵活和更容易控制。尽最大可能的减少对程序的整体的影响或产生副作用。特别是对已经存在的老的代码的完善和更新的情况下,这种处理方式是一种非常合理和便捷的存在。
当把一个操作看成一个黑盒时,它们内外的交互就不存在了,那么无论对哪一方来说,这都是好事儿。简单永远是开发者追求的目标!