本文是<functional>系列的第4篇。

成员指针是一个非常具有C++特色的功能。更低级的语言(如C)没有类,也就没有成员的概念;更高级的语言(如Java)没有指针,即使有也不会有成员指针这么拗口的东西。

上回在Stack Overflow上看到一个问题,C++是否允许delegate = object.method这种写法。我猜他是从C#过来的。在C++中,这种写法在语法上是不可能的,语义上可以用std::bind来实现。而本文的主题std::mem_fn,则是实现了delegate = method的功能,object插到了原来的参数列表的前面,成为新的函数对象的第一个参数。

成员指针

先说说成员指针。成员指针,分为对象成员指针与成员函数指针。下面的程序演示了如何定义和使用它们:

struct Test
{
int object;
void function(int) { }
}; int main()
{
Test test;
Test* ptr = &test;
int Test::* po = &Test::object;
test.*po;
ptr->*po;
void (Test::*pf)(int) = &Test::function;
(test.*pf)(0);
(ptr->*pf)(0);
}

定义为static的对象或函数,就好像它所在的类不存在一样,只能用普通的指针与函数指针。

这一节的重点在于成员指针的模板匹配。首先,形如对象成员指针的类型可以匹配成员函数指针:

template<typename>
struct member_test; template<typename Res, typename Class>
struct member_test<Res Class::*>
{
using result_type = Res;
using class_type = Class;
};
struct Test
{
int object;
void function(int) { }
}; using ObjectType = decltype(&Test::object);
using FunctionType = decltype(&Test::function); static_assert(std::is_same<
typename member_test<ObjectType>::result_type,
int>::value, "");
static_assert(std::is_same<
typename member_test<ObjectType>::class_type,
Test>::value, "");
static_assert(std::is_same<
typename member_test<FunctionType>::result_type,
void(int)>::value, "");
static_assert(std::is_same<
typename member_test<FunctionType>::class_type,
Test>::value, "");

ObjectType可以匹配Res Class::*,其中Res为int,Class为Test,这完全符合预期。令人震惊的是,FunctionType也可以匹配Res Class::*!其中Class依然为Test,而Res为函数类型void(int)。

那么是否可以写一个类模板,只能匹配成员函数指针而无法匹配对象成员指针呢?在此之前,为了能够更有说服力地用static_assert表示一个类没有result_type成员类型(而不是在编译错误后把代码注释掉),我写了个has_result_type类型,用的是昨天刚写过的void_t技巧:

template<typename T, typename = void>
struct has_result_type
: std::false_type { }; template<typename T>
struct has_result_type<T, std::void_t<typename T::result_type>>
: std::true_type { };

只匹配成员函数指针,需要加上一个可变参数:

template<typename>
struct member_function_test; template<typename Res, typename Class, typename... Args>
struct member_function_test<Res (Class::*)(Args...)>
{
using result_type = Res;
using class_type = Class;
}; static_assert(!has_result_type<
member_function_test<ObjectType>>::value, "");
static_assert(has_result_type<
member_function_test<FunctionType>>::value, "");
static_assert(std::is_same<
typename member_function_test<FunctionType>::result_type,
void>::value, "");

那么只匹配对象成员指针呢?很简单,只需写一个全部匹配的,再去掉成员函数指针即可:

template<typename>
struct member_object_test; template<typename Res, typename Class>
struct member_object_test<Res Class::*>
{
using result_type = Res;
using class_type = Class;
}; template<typename Res, typename Class, typename... Args>
struct member_object_test<Res (Class::*)(Args...)> { }; static_assert(has_result_type<
member_object_test<ObjectType>>::value, "");
static_assert(!has_result_type<
member_object_test<FunctionType>>::value, "");
static_assert(std::is_same<
typename member_object_test<ObjectType>::result_type,
int>::value, "");

如果成员函数有const或&会怎样?

struct Test
{
int object;
void function(int) { }
void function_const(int) const { }
void function_ref(int) & { }
}; static_assert(std::is_same<
typename member_test<decltype(&Test::function_const)>::result_type,
void(int) const>::value, "");
static_assert(std::is_same<
typename member_test<decltype(&Test::function_const)>::class_type,
Test>::value, "");
static_assert(std::is_same<
typename member_test<decltype(&Test::function_ref)>::result_type,
void(int) &>::value, "");
static_assert(std::is_same<
typename member_test<decltype(&Test::function_ref)>::class_type,
Test>::value, "");

Res Class::*中的Class还是不变,但是Res变成了后加const和&的函数类型。关于这两个类型我没有查到相关资料,只知道它们的std::is_function_v为true。不过这就够了。

mem_fn

懒得写了,照搬cppreference上的代码:

#include <functional>
#include <iostream> struct Foo {
void display_greeting() {
std::cout << "Hello, world.\n";
}
void display_number(int i) {
std::cout << "number: " << i << '\n';
}
int data = 7;
}; int main() {
Foo f; auto greet = std::mem_fn(&Foo::display_greeting);
greet(f); auto print_num = std::mem_fn(&Foo::display_number);
print_num(f, 42); auto access_data = std::mem_fn(&Foo::data);
std::cout << "data: " << access_data(f) << '\n';
}

输出:

Hello, world.
number: 42
data: 7

我寻思着你能读到这儿也不用我介绍std::mem_fn了吧,我的心思在它的实现上。

顺便提醒,不要跟std::mem_fun搞混,那玩意儿是C++98的化石。

实现

std::mem_fn基于std::invoke,std::invoke又基于std::result_of,所以从std::result_of讲起。

SFINAE

在C++中,检查一句语句是否合法有三种方式:目测、看编译器给不给error、SFINAE。对于模板代码,Visual Studio都智能不起来,更别说目测了;我们又不想看到编译器的error,所以得学习SFINAE,Substitution Failure Is Not An Error,替换失败不是错误。

struct __result_of_other_impl
{
template<typename _Fn, typename... _Args>
static __result_of_success<decltype(
std::declval<_Fn>()(std::declval<_Args>()...)
), __invoke_other> _S_test(int); template<typename...>
static __failure_type _S_test(...);
}; template<typename _Functor, typename... _ArgTypes>
struct __result_of_impl<false, false, _Functor, _ArgTypes...>
: private __result_of_other_impl
{
typedef decltype(_S_test<_Functor, _ArgTypes...>(0)) type;
};

__result_of_other_impl里有两个重载函数_S_test,__result_of_impl通过decltype获得它的返回类型。当_Functor(_ArgTypes...)语句合法时,第一个_S_test安好,int优于...,重载决议为第一个,type定义为_S_test前面一长串;不合法时,第一个_S_test实例化失败,但是模板替换失败不是错误,编译器继续寻找正确的重载,找到第二个_S_test,它的变参模板和可变参数像黑洞一样吞噬一切调用,一定能匹配上,type定义为__failure_type。

后文中凡是出现_S_test的地方都使用了SFINAE的技巧。

result_of

// For several sfinae-friendly trait implementations we transport both the
// result information (as the member type) and the failure information (no
// member type). This is very similar to std::enable_if, but we cannot use
// them, because we need to derive from them as an implementation detail. template<typename _Tp>
struct __success_type
{ typedef _Tp type; }; struct __failure_type
{ }; /// result_of
template<typename _Signature>
class result_of; // Sfinae-friendly result_of implementation: #define __cpp_lib_result_of_sfinae 201210 struct __invoke_memfun_ref { };
struct __invoke_memfun_deref { };
struct __invoke_memobj_ref { };
struct __invoke_memobj_deref { };
struct __invoke_other { }; // Associate a tag type with a specialization of __success_type.
template<typename _Tp, typename _Tag>
struct __result_of_success : __success_type<_Tp>
{ using __invoke_type = _Tag; }; // [func.require] paragraph 1 bullet 1:
struct __result_of_memfun_ref_impl
{
template<typename _Fp, typename _Tp1, typename... _Args>
static __result_of_success<decltype(
(std::declval<_Tp1>().*std::declval<_Fp>())(std::declval<_Args>()...)
), __invoke_memfun_ref> _S_test(int); template<typename...>
static __failure_type _S_test(...);
}; template<typename _MemPtr, typename _Arg, typename... _Args>
struct __result_of_memfun_ref
: private __result_of_memfun_ref_impl
{
typedef decltype(_S_test<_MemPtr, _Arg, _Args...>(0)) type;
}; // [func.require] paragraph 1 bullet 2:
struct __result_of_memfun_deref_impl
{
template<typename _Fp, typename _Tp1, typename... _Args>
static __result_of_success<decltype(
((*std::declval<_Tp1>()).*std::declval<_Fp>())(std::declval<_Args>()...)
), __invoke_memfun_deref> _S_test(int); template<typename...>
static __failure_type _S_test(...);
}; template<typename _MemPtr, typename _Arg, typename... _Args>
struct __result_of_memfun_deref
: private __result_of_memfun_deref_impl
{
typedef decltype(_S_test<_MemPtr, _Arg, _Args...>(0)) type;
}; // [func.require] paragraph 1 bullet 3:
struct __result_of_memobj_ref_impl
{
template<typename _Fp, typename _Tp1>
static __result_of_success<decltype(
std::declval<_Tp1>().*std::declval<_Fp>()
), __invoke_memobj_ref> _S_test(int); template<typename, typename>
static __failure_type _S_test(...);
}; template<typename _MemPtr, typename _Arg>
struct __result_of_memobj_ref
: private __result_of_memobj_ref_impl
{
typedef decltype(_S_test<_MemPtr, _Arg>(0)) type;
}; // [func.require] paragraph 1 bullet 4:
struct __result_of_memobj_deref_impl
{
template<typename _Fp, typename _Tp1>
static __result_of_success<decltype(
(*std::declval<_Tp1>()).*std::declval<_Fp>()
), __invoke_memobj_deref> _S_test(int); template<typename, typename>
static __failure_type _S_test(...);
}; template<typename _MemPtr, typename _Arg>
struct __result_of_memobj_deref
: private __result_of_memobj_deref_impl
{
typedef decltype(_S_test<_MemPtr, _Arg>(0)) type;
}; template<typename _MemPtr, typename _Arg>
struct __result_of_memobj; template<typename _Res, typename _Class, typename _Arg>
struct __result_of_memobj<_Res _Class::*, _Arg>
{
typedef typename remove_cv<typename remove_reference<
_Arg>::type>::type _Argval;
typedef _Res _Class::* _MemPtr;
typedef typename conditional<__or_<is_same<_Argval, _Class>,
is_base_of<_Class, _Argval>>::value,
__result_of_memobj_ref<_MemPtr, _Arg>,
__result_of_memobj_deref<_MemPtr, _Arg>
>::type::type type;
}; template<typename _MemPtr, typename _Arg, typename... _Args>
struct __result_of_memfun; template<typename _Res, typename _Class, typename _Arg, typename... _Args>
struct __result_of_memfun<_Res _Class::*, _Arg, _Args...>
{
typedef typename remove_cv<typename remove_reference<
_Arg>::type>::type _Argval;
typedef _Res _Class::* _MemPtr;
typedef typename conditional<__or_<is_same<_Argval, _Class>,
is_base_of<_Class, _Argval>>::value,
__result_of_memfun_ref<_MemPtr, _Arg, _Args...>,
__result_of_memfun_deref<_MemPtr, _Arg, _Args...>
>::type::type type;
}; // _GLIBCXX_RESOLVE_LIB_DEFECTS
// 2219. INVOKE-ing a pointer to member with a reference_wrapper
// as the object expression // Used by result_of, invoke etc. to unwrap a reference_wrapper.
template<typename _Tp, typename _Up = typename decay<_Tp>::type>
struct __inv_unwrap
{
using type = _Tp;
}; template<typename _Tp, typename _Up>
struct __inv_unwrap<_Tp, reference_wrapper<_Up>>
{
using type = _Up&;
}; template<bool, bool, typename _Functor, typename... _ArgTypes>
struct __result_of_impl
{
typedef __failure_type type;
}; template<typename _MemPtr, typename _Arg>
struct __result_of_impl<true, false, _MemPtr, _Arg>
: public __result_of_memobj<typename decay<_MemPtr>::type,
typename __inv_unwrap<_Arg>::type>
{ }; template<typename _MemPtr, typename _Arg, typename... _Args>
struct __result_of_impl<false, true, _MemPtr, _Arg, _Args...>
: public __result_of_memfun<typename decay<_MemPtr>::type,
typename __inv_unwrap<_Arg>::type, _Args...>
{ }; // [func.require] paragraph 1 bullet 5:
struct __result_of_other_impl
{
template<typename _Fn, typename... _Args>
static __result_of_success<decltype(
std::declval<_Fn>()(std::declval<_Args>()...)
), __invoke_other> _S_test(int); template<typename...>
static __failure_type _S_test(...);
}; template<typename _Functor, typename... _ArgTypes>
struct __result_of_impl<false, false, _Functor, _ArgTypes...>
: private __result_of_other_impl
{
typedef decltype(_S_test<_Functor, _ArgTypes...>(0)) type;
}; // __invoke_result (std::invoke_result for C++11)
template<typename _Functor, typename... _ArgTypes>
struct __invoke_result
: public __result_of_impl<
is_member_object_pointer<
typename remove_reference<_Functor>::type
>::value,
is_member_function_pointer<
typename remove_reference<_Functor>::type
>::value,
_Functor, _ArgTypes...
>::type
{ }; template<typename _Functor, typename... _ArgTypes>
struct result_of<_Functor(_ArgTypes...)>
: public __invoke_result<_Functor, _ArgTypes...>
{ }; /// std::invoke_result
template<typename _Functor, typename... _ArgTypes>
struct invoke_result
: public __invoke_result<_Functor, _ArgTypes...>
{ };

std::result_of和std::invoke_result本质上是相同的,无非是模板参数_Functor(_ArgTypes...)和_Functor, _ArgTypes...的区别,前者在C++17中废弃,后者在C++17中加入。

__invoke_result借助_Functor的类型分为三种情况:

  • __result_of_impl<false, false, _Functor, _ArgTypes...>,可调用对象类型不是成员指针,继承__result_of_other_impl,后者在上一节介绍过了;

  • __result_of_impl<true, false, _MemPtr, _Arg>,可调用对象是对象成员指针,继承__result_of_memobj:

    • 当_Argval与_Class相同或_Class是_Argval的基类时(其实is_base_of就可以概括这种关系;子类成员可以调用基类成员指针),使用__result_of_memobj_ref,调用方式为.*;

    • 否则,调用参数是个指针,使用__result_of_memobj_deref,调用方式为->*;

  • __result_of_impl<false, true, _MemPtr, _Arg, _Args...>,可调用对象是成员函数指针,详细讨论与上一种情况类似,不再赘述。

总之,对于合法的调用类型,__invoke_result最后继承到__success_type,定义type为返回类型;否则继承__failure_type,没有type成员。

Tag Dispatching

你注意到了吗?__result_of_success把__success_type包装了一下,加入了_Tag模板参数并定义为__invoke_type。在随后的实例化中,__invoke_type都是以下5个类型之一:

struct __invoke_memfun_ref { };
struct __invoke_memfun_deref { };
struct __invoke_memobj_ref { };
struct __invoke_memobj_deref { };
struct __invoke_other { };

这些类型极大地简化了__invoke的实现:

// Used by __invoke_impl instead of std::forward<_Tp> so that a
// reference_wrapper is converted to an lvalue-reference.
template<typename _Tp, typename _Up = typename __inv_unwrap<_Tp>::type>
constexpr _Up&&
__invfwd(typename remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Up&&>(__t); } template<typename _Res, typename _Fn, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_other, _Fn&& __f, _Args&&... __args)
{ return std::forward<_Fn>(__f)(std::forward<_Args>(__args)...); } template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_ref, _MemFun&& __f, _Tp&& __t,
_Args&&... __args)
{ return (__invfwd<_Tp>(__t).*__f)(std::forward<_Args>(__args)...); } template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t,
_Args&&... __args)
{
return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
} template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_ref, _MemPtr&& __f, _Tp&& __t)
{ return __invfwd<_Tp>(__t).*__f; } template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_deref, _MemPtr&& __f, _Tp&& __t)
{ return (*std::forward<_Tp>(__t)).*__f; } /// Invoke a callable object.
template<typename _Callable, typename... _Args>
constexpr typename __invoke_result<_Callable, _Args...>::type
__invoke(_Callable&& __fn, _Args&&... __args)
noexcept(__is_nothrow_invocable<_Callable, _Args...>::value)
{
using __result = __invoke_result<_Callable, _Args...>;
using __type = typename __result::type;
using __tag = typename __result::__invoke_type;
return std::__invoke_impl<__type>(__tag{}, std::forward<_Callable>(__fn),
std::forward<_Args>(__args)...);
} /// Invoke a callable object.
template<typename _Callable, typename... _Args>
inline invoke_result_t<_Callable, _Args...>
invoke(_Callable&& __fn, _Args&&... __args)
noexcept(is_nothrow_invocable_v<_Callable, _Args...>)
{
return std::__invoke(std::forward<_Callable>(__fn),
std::forward<_Args>(__args)...);
}

__invoke中定义这个__invoke_type为__tag,然后调用__invoke_impl时把__tag的实例传入,根据__tag的类型,编译器将重载函数决议为5个__invoke_impl中对应的那个。

这种技巧称为tag dispatching,我在std::function中也介绍过。

mem_fn

template<typename _MemFunPtr,
bool __is_mem_fn = is_member_function_pointer<_MemFunPtr>::value>
class _Mem_fn_base
: public _Mem_fn_traits<_MemFunPtr>::__maybe_type
{
using _Traits = _Mem_fn_traits<_MemFunPtr>; using _Arity = typename _Traits::__arity;
using _Varargs = typename _Traits::__vararg; template<typename _Func, typename... _BoundArgs>
friend struct _Bind_check_arity; _MemFunPtr _M_pmf; public: using result_type = typename _Traits::__result_type; explicit constexpr
_Mem_fn_base(_MemFunPtr __pmf) noexcept : _M_pmf(__pmf) { } template<typename... _Args>
auto
operator()(_Args&&... __args) const
noexcept(noexcept(
std::__invoke(_M_pmf, std::forward<_Args>(__args)...)))
-> decltype(std::__invoke(_M_pmf, std::forward<_Args>(__args)...))
{ return std::__invoke(_M_pmf, std::forward<_Args>(__args)...); }
}; template<typename _MemObjPtr>
class _Mem_fn_base<_MemObjPtr, false>
{
using _Arity = integral_constant<size_t, 0>;
using _Varargs = false_type; template<typename _Func, typename... _BoundArgs>
friend struct _Bind_check_arity; _MemObjPtr _M_pm; public:
explicit constexpr
_Mem_fn_base(_MemObjPtr __pm) noexcept : _M_pm(__pm) { } template<typename _Tp>
auto
operator()(_Tp&& __obj) const
noexcept(noexcept(std::__invoke(_M_pm, std::forward<_Tp>(__obj))))
-> decltype(std::__invoke(_M_pm, std::forward<_Tp>(__obj)))
{ return std::__invoke(_M_pm, std::forward<_Tp>(__obj)); }
}; template<typename _MemberPointer>
struct _Mem_fn; // undefined template<typename _Res, typename _Class>
struct _Mem_fn<_Res _Class::*>
: _Mem_fn_base<_Res _Class::*>
{
using _Mem_fn_base<_Res _Class::*>::_Mem_fn_base;
}; template<typename _Tp, typename _Class>
inline _Mem_fn<_Tp _Class::*>
mem_fn(_Tp _Class::* __pm) noexcept
{
return _Mem_fn<_Tp _Class::*>(__pm);
}

std::mem_fn返回类型为_Mem_fn,_Mem_fn继承_Mem_fn_base,后者分对象成员指针与成员函数指针两种情况,operator()都转发参数调用__invoke。

成员指针与mem_fn的更多相关文章

  1. C++ Primer 笔记——类成员指针

    1.当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据.成员指针指定了成员而非成员所属的对象,只有当解引用成员指针时,我们才提供对象信息. 2.和普通的函数指针类似,如果成员存在重载 ...

  2. C/C++基础----特殊工具和技术 (重载new和delete,RTT,限定作用域的枚举类型,类成员指针,嵌套类,局部类,volatile,链接指示 extern “C”)

    重载new和delete 1调用operator new( 或new[])标准库函数分配足够大的.原始的.未命名的内存空间以便存储特定类型的对象 2编译器运行相应地构造函数以构造这些对象,并为其传入初 ...

  3. c++_day5_成员指针

    1.成员指针实质:特定成员变量在对象实例中的相对地址. 2.类内可以直接初始化静态常量(声明部分).

  4. C/C++杂记:深入理解数据成员指针、函数成员指针

    1. 数据成员指针 对于普通指针变量来说,其值是它所指向的地址,0表示空指针. 而对于数据成员指针变量来说,其值是数据成员所在地址相对于对象起始地址的偏移值,空指针用-1表示.例: 代码示例: str ...

  5. C++中的类成员指针

    写在前面:本博客为本人原创,严禁任何形式的转载!本博客只允许放在博客园(.cnblogs.com),如果您在其他网站看到这篇博文,请通过下面这个唯一的合法链接转到原文! 本博客全网唯一合法URL:ht ...

  6. 【转载】C/C++杂记:深入理解数据成员指针、函数成员指针

    原文:C/C++杂记:深入理解数据成员指针.函数成员指针 1. 数据成员指针 对于普通指针变量来说,其值是它所指向的地址,0表示空指针.而对于数据成员指针变量来说,其值是数据成员所在地址相对于对象起始 ...

  7. C++成员指针

    C++中,成员指针是最为复杂的语法结构.但在事件驱动和多线程应用中被广泛用于调用回叫函数.在多线程应用中,每个线程都通过指向成员函数的指针来调用该函数.在这样的应用中,如果不用成员指针,编程是非常困难 ...

  8. 拷贝构造和拷贝赋值、静态成员(static)、成员指针、操作符重载(day06)

    十七 拷贝构造和拷贝赋值 浅拷贝和深拷贝 )如果一个类中包含指针形式的成员变量,缺省的拷贝构造函数只是复制了指针变量的本身,而没有复制指针所指向的内容,这种拷贝方式称为浅拷贝. )浅拷贝将导致不同对象 ...

  9. C++ 类成员指针

    C++的类成员指针是一种奇葩的指针. 假设现在我们要表示一个三维的点,现在有两种定义方式: struct point1{ int x, y, z; }; struct point2{ int c[3] ...

随机推荐

  1. Redis 按正则获取keys

    首先,我被坑了很久由于不知道这个redis支持的正则只有3种 1. * 任意长度的任意字符 2. ? 任意单一字符 3. [xxx] 匹配方括号中的一个字符 2.从上面开来,keys的模糊匹配功能很方 ...

  2. c++源文件从文本阶段到可执行文件的过程

    过程分为四个阶段: 预处理阶段····>编译阶段····>汇编阶段····>链接阶段 1)预处理阶段:对源代码文件中的文件包含关系.预编译语句(宏定义)进行分析和替换,生成预编译文件 ...

  3. 原 c++中map与unordered_map的区别

    c++中map与unordered_map的区别 头文件 map: #include < map > unordered_map: #include < unordered_map ...

  4. CF 631C report

    Each month Blake gets the report containing main economic indicators of the company "Blake Tech ...

  5. arcgis server建完站点之后修改默认6080端口号

    1.首先找到arcgis server的安装路径,找到server.xml文件,修改其中一处的6080端口为你想更改的端口号,例如8888.具体操作如下图所示: 默认的安装路径为:D:\Program ...

  6. ConcurrentHashMap1.7和1.8的源码分析比较

    ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap,为 ...

  7. 如何获取 bing 每日壁纸(超高清版)

    目录 需求描述 实现方式 简单粗暴 如何下载 如何更高清 排坑指南 初级 优点 给有好奇心的孩子 进阶 接口 自动保存 网站集成 爬虫 需求描述 必应作为一个在壁纸圈做搜索引擎最优秀的站点,其每日壁纸 ...

  8. Bootstrap Blazor 组件库

    项目介绍 Blazor 是一个使用 .NET 生成交互式客户端 Web UI 的框架: 使用 C# 代替 JavaScript 来创建丰富的交互式 UI. 共享使用 .NET 编写的服务器端和客户端应 ...

  9. NKOJ3772 看电影

    问题描述 共有m部电影,编号为1~m,第i部电影的好看值为w[i]. 在n天之中(从1~n编号)每天会放映一部电影,第i天放映的是第f[i]部. 你可以选择l,r(1<=l<=r<= ...

  10. 接口测试彻底弄懂Session、Cookie、Token的区别及联系hold住面试官--hold住了开3万,hold不住开3K!

    一.前言:接口测试之伤:cookie,session,token本是一家! cookie,session,token的区别早就已经成为测试同行的心病,各大论坛,各大博客,各大视频网站无不充斥着各种疑问 ...