目录
1.简介
std::not_fn
是 C++ 标准库中的一个函数对象适配器,它用于创建一个否定的函数对象。它的定义如下:
std::not_fn
接受一个可调用对象(如函数指针、函数对象或 lambda 表达式),并返回一个新的可调用对象。当调用这个新的可调用对象时,它会调用原始的可调用对象,并对其返回值进行逻辑取反操作。
这里面的f参数是一个Callable对象。它的受限条件为:
1) std::decay_t 须为可调用 (Callable) 并支持移动构造 (MoveConstructible)
2) std::is_constructible_v, F> 的结果必须为 true
那么,问题就来了,为什么要搞这么简单的一个东西呢?直接操作不更简单么?
老生再次常谈一下,一切的技术的应用,是跟场景结合起来而无法独立出来的(但可以抽象出来)。也就是说,std::not_fn的应用,也不是放置四海皆优秀的。在实际的开发中,可能会遇到很多种情况,比如普通的函数,类成员函数,仿函数,甚至新标准中的Lambda表达式等等。
在处理这些情况的时候儿,可能直接操作返回一个非的结果很简单,也可能比较不简单。更有可能虽然简单但不好理解。而有的情况下开发者需要的不是一个简单的结果而是一个非的函数,凡此种种,都可能会有不同的需要。
一般来说这种在上层进行封装的应用,大多数情况下在底层应用比较多,比如本身就是库或框架。一如前面看到的STL中的元函数(如std::is_integral等),在业务层展现应用的机会很少,但一旦到底层的库编程,则应用大行其道。
2.使用
下面看一个cppreference的例程:
#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的一种非的反向控制。它可以提供更灵活的控制方式,而不必直接修改相关的代码。
3.实现原理
在VS2019上是怎么实现上面的功能呢?不隐藏,直接上源码:
template<class _Callable>
_NODISCARD _Not_fn<decay_t<_Callable>> not_fn(_Callable&& _Obj)
_NOEXCEPT_COND(is_nothrow_constructible_v<decay_t<_Callable>, _Callable>) // strengthened
{ // wrap a callable object to be negated
return (_Not_fn<decay_t<_Callable>>(_STD forward<_Callable>(_Obj), _Not_fn_tag{}));
}
#endif /* _HAS_CXX17 */
在上面的代码中使用了 decay_t 剥离类型并使用 is_nothrow_constructible_v 限制了 _Obj 的类型,从而也印证了第一章节中的std::not_fn的使用限制条件。
std::not_fn返回了一个包装类_Not_fn,在_Not_fn重载operator()来实现可调用对象的调用,实现代码如下:
// FUNCTION TEMPLATE not_fn
struct _Not_fn_tag
{
};
template<class _Decayed>
class _Not_fn
: private _Ebco_base<_Decayed>
{ // wrap a callable object to be negated
private:
using _Mybase = _Ebco_base<_Decayed>;
public:
template<class _Callable,
class _Tag,
enable_if_t<is_same_v<_Tag, _Not_fn_tag>, int> = 0>
explicit _Not_fn(_Callable&& _Obj, _Tag)
_NOEXCEPT_COND(is_nothrow_constructible_v<_Decayed, _Callable>) // strengthened
: _Mybase(_STD forward<_Callable>(_Obj))
{ // store a callable object
}
_Not_fn(const _Not_fn&) = default;
_Not_fn(_Not_fn&&) = default;
template<class... _Types>
auto operator()(_Types&&... _Args) &
#ifndef __EDG__ // TRANSITION, VSO#604398
_NOEXCEPT_COND(_NOEXCEPT_OPER(!_STD invoke(this->_Get_val(),
_STD forward<_Types>(_Args)...))) // strengthened
#endif /* __EDG__ */
-> decltype(!_STD declval<invoke_result_t<_Decayed&, _Types...>>())
{ // call modifiable lvalue
return (!_STD invoke(this->_Get_val(), _STD forward<_Types>(_Args)...));
}
template<class... _Types>
auto operator()(_Types&&... _Args) const &
#ifndef __EDG__ // TRANSITION, VSO#604398
_NOEXCEPT_COND(_NOEXCEPT_OPER(!_STD invoke(this->_Get_val(),
_STD forward<_Types>(_Args)...))) // strengthened
#endif /* __EDG__ */
-> decltype(!_STD declval<invoke_result_t<const _Decayed&, _Types...>>())
{ // call const lvalue
return (!_STD invoke(this->_Get_val(), _STD forward<_Types>(_Args)...));
}
template<class... _Types>
auto operator()(_Types&&... _Args) &&
#ifndef __EDG__ // TRANSITION, VSO#604398
_NOEXCEPT_COND(_NOEXCEPT_OPER(!_STD invoke(_STD move(this->_Get_val()),
_STD forward<_Types>(_Args)...))) // strengthened
#endif /* __EDG__ */
-> decltype(!_STD declval<invoke_result_t<_Decayed, _Types...>>())
{ // call modifiable rvalue
return (!_STD invoke(_STD move(this->_Get_val()), _STD forward<_Types>(_Args)...));
}
template<class... _Types>
auto operator()(_Types&&... _Args) const &&
#ifndef __EDG__ // TRANSITION, VSO#604398
_NOEXCEPT_COND(_NOEXCEPT_OPER(!_STD invoke(_STD move(this->_Get_val()),
_STD forward<_Types>(_Args)...))) // strengthened
#endif /* __EDG__ */
-> decltype(!_STD declval<invoke_result_t<const _Decayed, _Types...>>())
{ // call const rvalue
return (!_STD invoke(_STD move(this->_Get_val()), _STD forward<_Types>(_Args)...));
}
};
从代码中可以看到,最终还是调用std::invoke来实现可调用对象的调用。
_Not_fn继承了_Ebco_base,_Ebco_base的实现比较巧妙,利用了空基类优化的思想,实现代码如下:
template<class _Ty,
bool = is_empty_v<_Ty> && !is_final_v<_Ty>>
class _Ebco_base
: private _Ty
{ // Empty Base Class Optimization, active
private:
using _Mybase = _Ty; // for visualization
protected:
template<class _Other,
enable_if_t<!is_same_v<remove_cv_t<remove_reference_t<_Other>>, _Ebco_base>, int> = 0>
explicit _Ebco_base(_Other&& _Val) _NOEXCEPT_COND(is_nothrow_constructible_v<_Ty, _Other>)
: _Ty(_STD forward<_Other>(_Val))
{
}
_Ty& _Get_val() noexcept
{
return (*this);
}
const _Ty& _Get_val() const noexcept
{
return (*this);
}
};
template<class _Ty>
class _Ebco_base<_Ty, false>
{ // Empty Base Class Optimization, inactive
private:
_Ty _Myval;
protected:
template<class _Other,
enable_if_t<!is_same_v<remove_cv_t<remove_reference_t<_Other>>, _Ebco_base>, int> = 0>
explicit _Ebco_base(_Other&& _Val) _NOEXCEPT_COND(is_nothrow_constructible_v<_Ty, _Other>)
: _Myval(_STD forward<_Other>(_Val))
{
}
_Ty& _Get_val() noexcept
{
return (_Myval);
}
const _Ty& _Get_val() const noexcept
{
return (_Myval);
}
};
4.特点
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并没有优势
所以开发者的头脑中始终要有根弦警惕着,技术要善用而非乱用!
5.总结
std::not_fn本身并没有什么难度。但只要认真想一下,其实就难明白,它其实就是让编程变得更灵活和更容易控制。尽最大可能的减少对程序的整体的影响或产生副作用。特别是对已经存在的老的代码的完善和更新的情况下,这种处理方式是一种非常合理和便捷的存在。
参考:https://zh.cppreference.com/w/cpp/utility/functional/not_fn