Lua C API中的迷惑行为 | Lua C API Confusions

There are some very confusing behaviors in Lua C API. Here are some explanations for these. (Tested on Lua 5.4) 1. Type of number, integer, string Operations like lua_isnumber, lua_pushinteger, lua_isstring don’t mean checking the value’s type: Values with type number can also get true of lua_isstring. (Number is also string) There are no integer types in Lua. Values generated by lua_pushinteger have type number. Only values generated by lua_pushinteger can get true of lua_isinteger. (But get false after lua_tostring) lua_isnumber gets true if the value is a real number or a string but convertible to number. lua_tostring will implicitly convert number to string: After calling lua_tostring on a value of type number, the value’s type will change to string implicitly, but it is still lua_isnumber. After calling lua_tostring on a value generated by lua_pushinteger, it cannot get true of lua_isinteger anymore. (String is always not integer) Test cases Lua value generated by lua_type lua_typename lua_isinteger lua_isnumber lua_isstring lua_pushinteger LUA_TNUMBER : 3 number true true true lua_pushinteger then lua_tostring LUA_TSTRING : 4 string false true true lua_pushnumber[1] LUA_TNUMBER : 3 number false true true lua_pushnumber[1] then lua_tostring LUA_TSTRING : 4 string false true true lua_pushstring on string of number[2] LUA_TSTRING : 4 string false true true lua_pushstring on string of not number[3] LUA_TSTRING : 4 string false false true [1] Applies to all float or integer numbers. e.g. lua_pushnumber(L, 0), lua_pushnumber(L, 123), lua_pushnumber(L, 0.1), etc. [2] e.g. “123”, “1.23”, “12.”, “.12”, “-1.23”, “+1.23”, “1.2e3”, “-1.2E-3”, etc. [3] e.g. “1.2.3”, “1+2”, “hello”, “inf”, etc. Conclusion Integer is not a type. Integer is a special case of number. Number is also string. String is also number if it is convertible to number. String is always not integer. lua_tostring will implicitly convert number to string. More to say Not like lua_tostring, luaL_tolstring won’t convert number to string. But it can convert any value to string, while lua_tostring will return NULL if the value is not a string or number. 2. Type of userdata, light userdata Userdata and light userdata have different type ID, but they have a same type name. lua_islightuserdata is a subset of lua_isuserdata: lua_isuserdata returns true for both userdata and light userdata. (Light userdata is userdata) lua_islightuserdata only returns true for light userdata. Test cases Lua value generated by lua_type lua_typename lua_islightuserdata lua_isuserdata lua_pushlightuserdata LUA_TLIGHTUSERDATA : 2 userdata true true lua_newuserdata LUA_TUSERDATA : 7 userdata false true Conclusion Type ID of light userdata is “LUA_TLIGHTUSERDATA” (value 2), type ID of userdata is “LUA_TUSERDATA” (value 7), but they have a same type name “userdata”. Light userdata is a subset of userdata, although they have different type ID. Light userdata is userdata. 3. Metatable of light userdata Light userdata (unlike heavy userdata) have no per-value metatables. All light userdata share the same metatable, which by default is not set (nil).

2024/10/22
articleCard.readMore

"nullptr"是指针类型吗?如何用C++的方式把"T*"转换成"void*" | Is "nullptr" a Pointer Type? How to Convert "T*" to "void*" by C++ Way

nullptr是指针类型吗? nullptr是C++里预定义的一个变量,它的类型是std::nullptr_t。 判断一个类型是否是指针类型,可以用std::is_pointer来判断。 测试std::nullptr_t是否是指针类型的代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <myostream.h> myostream::ostream mycout(std::cout.rdbuf()); #define watch(...) MYOSTREAM_WATCH(mycout, " = ", "\n", "\n\n", __VA_ARGS__) int main() { // 确认nullptr就是std::nullptr_t类型 static_assert(std::is_same_v<decltype(nullptr), std::nullptr_t>, "Never happen"); watch(std::is_pointer_v<std::nullptr_t>, std::is_member_pointer_v<std::nullptr_t>, std::is_pointer_v<std::nullptr_t *> ); return 0; } 输出: 1 2 3 std::is_pointer_v<std::nullptr_t> = 0 std::is_member_pointer_v<std::nullptr_t> = 0 std::is_pointer_v<std::nullptr_t *> = 1 可见std::nullptr_t并不是一个指针类型。 只不过我们平时常用nullptr来赋值给任意指针类型,会给人一种std::nullptr_t是指针类型的错觉。 从上例中还可以看出std::nullptr_t *等类型是指针类型,它们是指向std::nullptr_t的指针类型。 原理 std::nullptr_t不是指针类型,但是却可以转换成任意指针类型,并且以0为值。 它的一种实现方式如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #ifdef _LIBCPP_HAS_NO_NULLPTR _LIBCPP_BEGIN_NAMESPACE_STD struct _LIBCPP_TEMPLATE_VIS nullptr_t { void* __lx; struct __nat {int __for_bool_;}; _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t() : __lx(0) {} _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t(int __nat::*) : __lx(0) {} _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator int __nat::*() const {return 0;} template <class _Tp> _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator _Tp* () const {return 0;} template <class _Tp, class _Up> _LIBCPP_INLINE_VISIBILITY operator _Tp _Up::* () const {return 0;} friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator==(nullptr_t, nullptr_t) {return true;} friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator!=(nullptr_t, nullptr_t) {return false;} }; inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t __get_nullptr_t() {return nullptr_t(0);} #define nullptr _VSTD::__get_nullptr_t() _LIBCPP_END_NAMESPACE_STD #else // _LIBCPP_HAS_NO_NULLPTR namespace std { typedef decltype(nullptr) nullptr_t; } #endif // _LIBCPP_HAS_NO_NULLPTR 可见std::nullptr_t不是指针类型,可能是类似类的类型。 所以当然可以构造出指向std::nullptr_t的指针类型了,例如std::nullptr_t *,const std::nullptr_t *等。 那么std::nullptr_t是类类型吗? nullptr是类类型吗? 通过以上源码可以看出std::nullptr_t通过宏开关设置了两种实现方式, 第一种就是一个普通的struct,是类类型,而第二种似乎是编译器内置的,不是类类型。 所以std::nullptr_t的类型是依赖实现的,可能是类类型,也可能不是。 判断一个类型是否是类类型,可用std::is_class。测试代码如下: 1 2 3 4 5 6 7 8 watch(std::is_class_v<std::nullptr_t>, std::is_union_v<std::nullptr_t>, std::is_null_pointer_v<std::nullptr_t>, std::is_scalar_v<std::nullptr_t>, std::is_object_v<std::nullptr_t>); watch(std::is_null_pointer_v<std::nullptr_t>, std::is_null_pointer_v<void*>); Clang和GCC在 -std=c++17 下默认输出是一样的: 1 2 3 4 5 6 7 8 std::is_class_v<std::nullptr_t> = 0 std::is_union_v<std::nullptr_t> = 0 std::is_null_pointer_v<std::nullptr_t> = 1 std::is_scalar_v<std::nullptr_t> = 1 std::is_object_v<std::nullptr_t> = 1 std::is_null_pointer_v<std::nullptr_t> = 1 std::is_null_pointer_v<void*> = 0 可见默认情况下std::nullptr_t也不是类类型,它就是独特的一个类型,可用std::is_null_pointer来判断。 (当然,如果在Clang下编译时手动添加宏定义_LIBCPP_HAS_NO_NULLPTR,那么std::is_class_v<std::nullptr_t>就等于true了,它就是普通的类类型了。) nullptr能被取地址吗? nullptr是右值临时变量,无法取地址,但是我们可以使用std::nullptr_t重新定义一个新的"nullptr"左值,然后就可以对其取地址了。代码如下: 1 2 3 4 5 // watch(nullptr, &nullptr); // error: cannot take the address of an rvalue of type 'nullptr_t' std::nullptr_t null1, null2; watch(null1, &null1); watch(null2, &null2); 输出: 1 2 3 4 5 null1 = nullptr &null1 = 0x16b1d3578 null2 = nullptr &null2 = 0x16b1d3570 如何用C++的方式把"T*"转换成"void*" 假如T是某种未知的模版类型,如何把T*的变量转换成void*类型呢? 用C语言的强制类型转换很容易实现: 1 2 3 4 template <typename T> void* c_cast(T* p) { // 使用C风格的强制类型转换 return (void*)(p); } 那么如何用C++的类型转换方式实现呢? 事实上只用reinterpret_cast或static_cast是不行的,因为它们不能将 cv-qualified pointer 转换成 cv-unqualified pointer。 所以必须首先使用const_cast将cv属性去掉,然后再使用reinterpret_cast或static_cast。如下: 1 2 3 4 5 6 7 template <typename T> void* cpp_cast(T* p) { // 使用C++风格的强制类型转换 // return reinterpret_cast<void*>(p); // error return reinterpret_cast<void*>(const_cast<std::remove_cv_t<T>*>(p)); // OK // 或者用static_cast亦可 // return static_cast<void*>(const_cast<std::remove_cv_t<T>*>(p)); // OK } cv-qualified std::nullptr_t及其指针 经过以上讨论,知道std::nullptr_t不是指针类型,那么如下两个问题就可迎刃而解。 cv-qualified std::nullptr_t 类型的变量能直接赋值给 std::nullptr_t 类型的变量吗? 可以。 1 2 const volatile std::nullptr_t p{0}; std::nullptr_t np = p; // OK cv-qualified std::nullptr_t * 类型的变量能直接赋值给 std::nullptr_t * 类型的变量吗? 不可以。需要用const_cast转换后方可赋值。 1 2 3 const volatile std::nullptr_t * p{0}; // std::nullptr_t* np = p; // Error std::nullptr_t* np = const_cast<std::nullptr_t*>(p); // OK

2023/6/15
articleCard.readMore

成为Contributor | To Be a Contributor

说来惭愧,身为一名程序员,从业五年多,竟然最近一年才开始在开源世界为他人的项目贡献代码。 最近我在阅读一个写得很棒的开源小代码库的时候发现他有一个typo错误 (这个错误竟然已经存在了9年了都没有被发现😂,而且是一旦运行到这里就会引发Exception的), 于是动手给他改正,并提了一个 PR 给维护者(虽然不是原作者),一天后就得到回复并最终被merge了。我很开心, 至此我第二次成为他人项目的contributor,虽然只是一个小小的typo fix,但其实要读懂一个 高级的代码并理解他的思想并不容易,特别是当你读一个比自己水平高很多的人写的代码的时候。 顺便提一下我第一次成为他人项目contributor是为其贡献一个小小的feature, see PR。 这两次经历其实是一脉相承的,起因是我想要寻找一个能在C++项目中使用的动态表达式解析器。 第一次我找到了一个相关的小项目并简单阅读了一下代码发现它并不能完全满足我的需求, 但看起来要满足我的一个需求其实改动并不复杂,于是我动手改了一下并提PR建议给维护者, 经过简单讨论以后这个PR竟然被接受了。那时我也很开心。 之后我又考虑其他方案,但最终我觉得有时间还是应该去了解一下那个成熟的嵌入式脚本语言Lua。 于是今年初我开始学习Lua(前年买的书终于开始读了……买书如山倒,看书如抽丝啊), 和世界上大多数知名的成熟的通用的项目一样, 他们为了通用性,并不会为某一种简单的具体的小应用场景贡献特别简单易用的API,顶多给出一个Example 让你看看学学怎么用。因此有些项目应用起来还有点小门槛呢! Lua也是这样,虽然它很简单,上手很容易,但是就是写起来稍微有点麻烦,于是我就想,学都学了, 就顺便写一个小小的Wrapper库让它用起来更简单方便吧。 于是不久我就写好了,正好也借此机会加深了对Lua的理解,也加强了熟练程度。 作为一名程序员,我信奉一句话: 想要成为一名优秀的程序员,你要多写代码,想要成为一名卓越的程序员,你要多读代码, 特别是要多读卓越的代码。 于是我又在Github上搜索类似的项目,然后就看到了这个几乎是十年前的项目,它是我目前搜到的同类中最好的。 它提供了一些令我感觉Amazing的功能,然后我就开始了阅读这个项目代码的坎坷旅途,区区两千余行代码, 竟然让我看了好几天😂,它击中了好多我的知识盲点,我是一边恶补知识盲点一边读这个代码的! 然后有一刻我发现有一个地方看着很奇怪,在深入看了这个点的上下文代码和相关功能的实现后, 我最终确认这是一个 copy then paste then forget to modify 的 typo, 于是便有了本文开头所说的事。 成为Contributor,虽然这是不值一文的小事,但却是我的一个小小的里程碑, 仿佛一个人的出生一样的小小的里程碑。我想它是代表我不是只能写自己的代码, 而是可以加入开源大世界的小小里程碑吧,故此留念。 写出卓越的代码,贡献给开源世界,得到全世界程序员的使用和认可,从而帮助程序员们, 甚至不仅仅是程序员而是世上所有人们,帮助他们构建更美好的世界,是多少程序员们的终身追求啊! 作为程序员,我们应该信奉另一句话: Build the World!

2023/5/17
articleCard.readMore

查验C++类型推导结果 | Check C++ Type Deduction Result

如何查看类型 C++的类型系统是极其复杂的,基本类型与const, volatile, 指针,引用,数组,函数,类,成员变量, 成员函数等特性的组合能生成许多不同的类型。类型推导是C++泛型编程/模版元编程的基础。 那么如何查看某个类型或变量具体是什么类型呢?我使用的方法有 C++内置的typeid boost::typeindex::type_id 猜测类型并用std::is_same校验 故意使编译报错在编译器给出的错误信息中查看目标的类型 例如用typeid: 1 2 3 const int a[3] = {}; std::cout << typeid(decltype(a)).name() << std::endl; // prints: A3_i 可见使用typeid获取的类型名不具有好的可读性。于是可以用boost::typeindex::type_id替代: 1 2 3 4 5 // need include this header // #include <boost/type_index.hpp> const int a[3] = {}; std::cout << boost::typeindex::type_id<decltype(a)>().pretty_name() << std::endl; // prints: int [3] 可见虽然结果具有很好的可读性,但是却去掉了const属性,与真实输入类型不符合。 事实上,C++内置的typeid和Boost的type_id得出的结果都是对输入类型去掉了 const, volatile和引用属性的。 那有没有获取完整类型的方法呢? C++中可以用std::is_same来判断两个类型是否相同,它没有对输入类型做任何改变。 因此可以猜测一个结果然后用它来判断猜得是否正确: 1 2 3 4 5 if (std::is_same_v<decltype(a), int [3]>) { // a has type: int [3] } else if (std::is_same_v<decltype(a), const int [3]>) { // a has type: const int [3] } 但是这样太麻烦了,如果不想猜呢?由于编译器编译阶段是知道变量或表达式的类型的,所以我们可以故意 写某种会编译报错的代码,让我们想要查看的类型名展示在报错信息中: 1 2 3 4 5 6 7 8 template <typename> struct EMPTY{}; // CE: compilation error; NX: not exists #define CETYPE(type) EMPTY<type>::NX() int main() { const int a[3] = {}; CETYPE(decltype(a)); } 编译报错信息是: 1 error: no member named 'NX' in 'EMPTY<const int[3]>' 于是可以看出a的类型是"const int[3]"。 (也有编译器输出"int const[3]",这是等效的写法,它们是相同的类型) 类型推导示例 C function 预先定义好的用于类型推导的函数: 1 void func(int) {} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 { CETYPE(decltype(func)); // void (int) CETYPE(decltype(&func)); // void (*)(int) // implicitly convert to pointer if assigned by function name auto fcopy = func; CETYPE(decltype(fcopy)); // void (*)(int) const auto cfcopy = func; CETYPE(decltype(cfcopy)); // void (*const)(int) // same as above auto fptr = &func; CETYPE(decltype(fptr)); // void (*)(int) const auto cfptr = &func; CETYPE(decltype(cfptr)); // void (*const)(int) // static_cast on rvalue reference to function returns lvalue CETYPE(decltype(static_cast<void(&&)(int)>(func))); // void (&)(int) CETYPE(decltype(static_cast<void(&)(int)>(func))); // void (&)(int) // so, std::move on function returns lvalue reference CETYPE(decltype(std::move(func))); // void (&)(int) CETYPE(decltype(&std::move(func))); // void (*)(int) CETYPE(decltype(std::move(&func))); // void (*&&)(int) // explicitly define a rvalue reference to function decltype(func) && prfunc = func; CETYPE(decltype(prfunc)); // void (&&)(int) // again, std::move on rvalue reference to function also returns lvalue reference CETYPE(decltype(std::move(prfunc))); // void (&)(int) } // references { auto & lref = func; CETYPE(decltype(lref)); // void (&)(int) CETYPE(decltype(&lref)); // void (*)(int) const auto & clref = func; // 由于规定非成员函数不能有const属性,所以const被忽略掉 CETYPE(decltype(clref)); // void (&)(int) CETYPE(decltype(&clref)); // void (*)(int) auto && rref = func; CETYPE(decltype(rref)); // void (&)(int) CETYPE(decltype(&rref)); // void (*)(int) // ref of move const auto & clref_of_move = std::move(func); CETYPE(decltype(clref_of_move)); // void (&)(int) auto && rref_of_move = std::move(func); CETYPE(decltype(rref_of_move)); // void (&)(int) } // references of pointer { // &func is a rvalue pointer const auto & clref = &func; CETYPE(decltype(clref)); // void (*const &)(int) auto && rref = &func; CETYPE(decltype(rref)); // void (*&&)(int) // pfunc is a lvalue pointer auto pfunc = &func; // void (*)(int) const auto & clref = pfunc; CETYPE(decltype(clref)); // void (*const &)(int) auto && rref = pfunc; CETYPE(decltype(rref)); // void (*&)(int) // ref of move const auto & clref_of_move = std::move(pfunc); CETYPE(decltype(clref_of_move)); // void (*const &)(int) auto && rref_of_move = std::move(pfunc); CETYPE(decltype(rref_of_move)); // void (*&&)(int) } 定义如下两个模版函数,其参数声明分别是C++11以前的万能引用和C++11引入的新的万能引用。 1 2 3 4 5 6 7 8 template<typename T> void larg(const T & t) { CETYPE(T); CETYPE(decltype(t)); } template<typename T> void rarg(T && t) { CETYPE(T); CETYPE(decltype(t)); } 将函数func用作以上两个模版函数的参数,类型推导结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 { larg(func); // template<typename T> void larg(const T & t) { // CETYPE(T); // void (int) // CETYPE(decltype(t)); // void (&)(int) // } rarg(func); // template<typename T> void rarg(T && t) { // CETYPE(T); // void (&)(int) // CETYPE(decltype(t)); // void (&)(int) // } } { larg(std::move(func)); // template<typename T> void larg(const T & t) { // CETYPE(T); // void (int) // CETYPE(decltype(t)); // void (&)(int) // } rarg(std::move(func)); // template<typename T> void rarg(T && t) { // CETYPE(T); // void (&)(int) // CETYPE(decltype(t)); // void (&)(int) // } } { larg(&func); // template<typename T> void larg(const T & t) { // CETYPE(T); // void (*)(int) // CETYPE(decltype(t)); // void (*const &)(int) // } rarg(&func); // template<typename T> void rarg(T && t) { // CETYPE(T); // void (*)(int) // CETYPE(decltype(t)); // void (*&&)(int) // } } { auto pfunc = &func; // void (*)(int) larg(pfunc); // template<typename T> void larg(const T & t) { // CETYPE(T); // void (*)(int) // CETYPE(decltype(t)); // void (*const &)(int) // } rarg(pfunc); // template<typename T> void rarg(T && t) { // CETYPE(T); // void (*&)(int) // CETYPE(decltype(t)); // void (*&)(int) // } } { auto pfunc = &func; // void (*)(int) larg(std::move(pfunc)); // template<typename T> void larg(const T & t) { // CETYPE(T); // void (*)(int) // CETYPE(decltype(t)); // void (*const &)(int) // } rarg(std::move(pfunc)); // template<typename T> void rarg(T && t) { // CETYPE(T); // void (*)(int) // CETYPE(decltype(t)); // void (*&&)(int) // } } 小结 可见,函数模版参数const T & t与定义引用const auto & r = ...推导结果一致, 函数模版参数T && t与定义引用auto && r = ...推导结果一致。 特别的,std::move 或 std::forward 作用于函数类型时返回的总是 lvalue FunctionType& 而不是FunctionType&&。 这是因为 static_cast<FunctionType&&>(func) 返回的是 FunctionType& 而不是 FunctionType&&。 另外,由于C++规定不是成员函数的函数不能有const、valitale属性,所以 const auto & clref = func;得到clref的类型是void (&)(int),const属性被忽略了。 如果换成其他类型,则const属性会被保留,例如int,如下: Int 1 2 3 4 5 int i = 0; // const被保留 const auto & clref = i; CETYPE(decltype(clref)); // const int & CETYPE(decltype(&clref)); // const int * 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int i = 0; const int ci = 0; CETYPE(decltype(i)); // int CETYPE(decltype(ci)); // const int // 非单个标记符的左值,推导为T&。例如加一个小括号,或前置自增 CETYPE(decltype((i))); // int & CETYPE(decltype((ci))); // const int & CETYPE(decltype(++i)); // int & // 后置自增是一个右值,于是推导为T CETYPE(decltype(i++)); // int // 右值 CETYPE(decltype(i + ci)); // int // 右值引用 CETYPE(decltype(std::move(i))); // int && 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 larg(i); // template<typename T> void larg(const T & t) { // CETYPE(T); // int // CETYPE(decltype(t)); // const int & // } rarg(i); // template<typename T> void rarg(T && t) { // CETYPE(T); // int & // CETYPE(decltype(t)); // int & // } larg(ci); // template<typename T> void larg(const T & t) { // CETYPE(T); // int // CETYPE(decltype(t)); // const int & // } rarg(ci); // template<typename T> void rarg(T && t) { // CETYPE(T); // const int & // CETYPE(decltype(t)); // const int & // } larg(std::move(i)); // template<typename T> void larg(const T & t) { // CETYPE(T); // int // CETYPE(decltype(t)); // const int & // } rarg(std::move(ci)); // template<typename T> void rarg(T && t) { // CETYPE(T); // const int // CETYPE(decltype(t)); // const int && // } rarg(3); // template<typename T> void rarg(T && t) { // CETYPE(T); // int // CETYPE(decltype(t)); // int && // } C array 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 char a[3]; CETYPE(decltype(a)); // char[3] CETYPE(decltype(&a)); // char (*)[3] CETYPE(decltype(&a[0])); // char * const auto & acr = a; CETYPE(decltype(acr)); // const char (&)[3] const auto & pacr = &a; CETYPE(decltype(pacr)); // char (*const &)[3] const auto & pa0cr = &a[0]; CETYPE(decltype(pa0cr)); // char *const & auto && arr = a; CETYPE(decltype(arr)); // char (&)[3] auto && parr = &a; CETYPE(decltype(parr)); // char (*&&)[3] auto pa = &a; auto && lparr = pa; CETYPE(decltype(lparr)); // char (*&)[3] auto && pa0rr = &a[0]; CETYPE(decltype(pa0rr)); // char *&& auto pa0 = &a[0]; auto && lpa0rr = pa0; CETYPE(decltype(lpa0rr)); // char *& 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const char a[3]; CETYPE(decltype(a)); // const char[3] CETYPE(decltype(&a)); // const char (*)[3] CETYPE(decltype(&a[0])); // const char * const auto & acr = a; CETYPE(decltype(acr)); // const char (&)[3] const auto & pacr = &a; CETYPE(decltype(pacr)); // const char (*const &)[3] const auto & pa0cr = &a[0]; CETYPE(decltype(pa0cr)); // const char *const & auto && arr = a; CETYPE(decltype(arr)); // const char (&)[3] auto && parr = &a; CETYPE(decltype(parr)); // const char (*&&)[3] auto pa = &a; auto && lparr = pa; CETYPE(decltype(lparr)); // const char (*&)[3] auto && pa0rr = &a[0]; CETYPE(decltype(pa0rr)); // const char *&& auto pa0 = &a[0]; auto && lpa0rr = pa0; CETYPE(decltype(lpa0rr)); // const char *& 1 2 3 4 5 6 7 8 9 10 int ia[] = {}; int ia2[] = {1, 2}; int ia3[3] = {1}; CETYPE(decltype(ia)); // int[0] CETYPE(decltype(ia2)); // int[2] CETYPE(decltype(ia3)); // int[3] CETYPE(int[]); // int[] CETYPE(const int[]); // const int[] CETYPE(decltype(&ia)); // int (*)[0] C string 字面字符串常量,decltype推断结果如下: 1 2 3 4 CETYPE(decltype("abcd")); // const char (&)[5] auto s = "abcd"; CETYPE(decltype(s)); // const char * C++中的函数类型到底有多少种形式? 影响函数类型的修饰属性都是正交的,因此把每一种属性的选项数量乘起来就是答案。 根据一个函数类型是否是const的,是否是volatile的,是否有C语言风格的可变长实参, 是没有成员函数引用属性还是具有成员函数左值引用或成员函数右值引用 (请注意,这里一定是成员函数引用才算作函数类型签名,C语言函数的引用在此不算作函数类型), 还有C++17以后一个函数是否是noexcept也认作不同的函数类型, 所以根据这五种属性可以算出总共是2 * 2 * 2 * 3 * 2 = 48种! 判断一个类型是否是函数类型,C++标准库中定义了std::is_function来实现这个判断。 它的一种可能的实现方式就是把以上所说所有的函数类型形式都特化一遍,如下所示: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 // primary template template<typename> struct is_function : std::false_type { }; // specialization for regular functions template<typename Return, typename... Args> struct is_function<Return(Args...)> : std::true_type {}; // specialization for variadic functions such as std::printf template<typename Return, typename... Args> struct is_function<Return(Args..., ...)> : std::true_type {}; // specialization for function types that have cv-qualifiers template<typename Return, typename... Args> struct is_function<Return(Args...) const> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) volatile> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const volatile> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) volatile> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const volatile> : std::true_type {}; // specialization for function types that have ref-qualifiers template<typename Return, typename... Args> struct is_function<Return(Args...) &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) volatile &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const volatile &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) volatile &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const volatile &> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) volatile &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const volatile &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) volatile &&> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const volatile &&> : std::true_type {}; // specializations for noexcept versions of all the above (C++17 and later) template<typename Return, typename... Args> struct is_function<Return(Args...) noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) volatile noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const volatile noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) volatile noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const volatile noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) volatile & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const volatile & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) volatile & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const volatile & noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) volatile && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args...) const volatile && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) volatile && noexcept> : std::true_type {}; template<typename Return, typename... Args> struct is_function<Return(Args..., ...) const volatile && noexcept> : std::true_type {}; 针对上文特别提起的引用属性举个例子: 1 2 3 4 5 6 7 // 这两个是C语言函数的引用,判断为false std::is_function<void(&)(int)>::value; std::is_function<void(&&)(int)>::value; // 而这两个是成员函数的引用,判断为true std::is_function<void(int) &>::value; std::is_function<void(int) &&>::value; 对函数类型进行add pointer/c/v/r, remove c/v/r等操作是什么结果? 以上48种函数类型形式其实可分成两类。 第一类是C语言函数形式的函数类型,上文也提到,C++规定它们不允许有const和volatile属性, 当然它不是成员函数自然也不能有成员函数引用属性,只能有可变长实参属性和noexcept属性; 剩下的就是第二类,非C语言函数形式的函数类型,凡是具有const属性、volatile属性、 成员函数引用属性 的都属于这类,这一类更像是成员函数,但是不具有母类类型信息,不是完整的成员函数类型, 姑且叫成员函数形式的函数类型或者不完整成员函数类型吧。 用cppreference的说法,也可以说成第一类是"not cv- or ref- qualified function type", 第二类是"cv- or ref- qualified function type". 不过也许说成C语言函数形式和成员函数形式,理解它们一个是完整的,一个是不完整的, 可能能更好得理解它们为什么会有以下令人费解令人诧异的不同表现。 算一下,第一类有4种,第二类有44种。 add reference and remove reference 针对C语言函数形式的函数类型添加引用结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 CETYPE(std::add_lvalue_reference_t<void(int)>); // void (&)(int) CETYPE(std::add_rvalue_reference_t<void(int)>); // void (&&)(int) CETYPE(std::add_lvalue_reference_t<void(int, ...)>); // void (&)(int, ...) CETYPE(std::add_rvalue_reference_t<void(int, ...)>); // void (&&)(int, ...) CETYPE(std::add_lvalue_reference_t<void(int) noexcept>); // void (&)(int) noexcept CETYPE(std::add_rvalue_reference_t<void(int) noexcept>); // void (&&)(int) noexcept CETYPE(std::add_lvalue_reference_t<void(int, ...) noexcept>); // void (&)(int, ...) noexcept CETYPE(std::add_rvalue_reference_t<void(int, ...) noexcept>); // void (&&)(int, ...) noexcept // 引用折叠 CETYPE(std::add_lvalue_reference_t<void (&)(int)>); // void (&)(int) CETYPE(std::add_rvalue_reference_t<void (&)(int)>); // void (&)(int) CETYPE(std::add_lvalue_reference_t<void (&&)(int)>); // void (&)(int) CETYPE(std::add_rvalue_reference_t<void (&&)(int)>); // void (&&)(int) 可见这些函数类型都被添加了引用,不过应该注意这个是常规引用(某种类型整体的引用), 不是成员函数引用,这个引用不影响函数签名。 顺便提一下,对以上输出的带引用的类型施加std::remove_reference_t当然也会回到原输入类型。 针对成员函数形式的函数类型add和remove引用结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // add reference CETYPE(std::add_lvalue_reference_t<void(int) const>); // void (int) const CETYPE(std::add_rvalue_reference_t<void(int) volatile>); // void (int) volatile CETYPE(std::add_lvalue_reference_t<void(int, ...) volatile>); // void (int, ...) volatile CETYPE(std::add_rvalue_reference_t<void(int, ...) const>); // void (int, ...) const CETYPE(std::add_lvalue_reference_t<void(int) const noexcept>); // void (int) const noexcept CETYPE(std::add_rvalue_reference_t<void(int) volatile noexcept>); // void (int) volatile noexcept CETYPE(std::add_lvalue_reference_t<void(int) &>); // void (int) & CETYPE(std::add_rvalue_reference_t<void(int) &>); // void (int) & CETYPE(std::add_lvalue_reference_t<void(int) &&>); // void (int) && CETYPE(std::add_rvalue_reference_t<void(int) &&>); // void (int) && CETYPE(std::add_lvalue_reference_t<void(int) const &>); // void (int) const & CETYPE(std::add_rvalue_reference_t<void(int) volatile &>); // void (int) volatile & CETYPE(std::add_lvalue_reference_t<void(int) const && noexcept>); // void (int) const && noexcept // remove reference CETYPE(std::remove_reference_t<void(int) &>); // void (int) & CETYPE(std::remove_reference_t<void(int) &&>); // void (int) && CETYPE(std::remove_reference_t<void(int) const &>); // void (int) const & CETYPE(std::remove_reference_t<void(int) const &&>); // void (int) const && CETYPE(std::remove_reference_t<void(int) volatile &>); // void (int) volatile & CETYPE(std::remove_reference_t<void(int) & noexcept>); // void (int) & noexcept CETYPE(std::remove_reference_t<void(int) const & noexcept>); // void (int) const & noexcept CETYPE(std::remove_reference_t<void(int) volatile && noexcept>); // void (int) volatile && noexcept 可见无论是add还是remove reference,对输入的函数类型,都没有做任何更改。所以可得: C语言函数形式的函数类型可以添加引用,而成员函数形式的函数类型本身自带的引用属性不会被更改。 成员函数的引用,在这里由于成为了函数签名的一部分,属于函数类型的一部分,所以这时的引用就与普通常规引用大不相同, 比普通引用的地位更高一等,是不能通过std::add_lvalue_reference, std::remove_reference等被改变的。 那么这类引用就太特殊了,凭什么都是函数类型,一些能被添加引用和去除引用,而另一种不能呢? 是否应该把它定义为另外一类引用,或者另外起一个名字称为另外一类功能? 然后与常规引用不冲突,再可以添加常规引用呢?那么可能就会出现形如void (&)(int) const && 之类的东西了。😂复杂程度又增加了,不过好像又能够和第一类函数类型保持一致了,是好还是不好呢? C++的选择是不这么做,否则的话,还要再提供一种成员引用的功能与之匹配。C++现在已经有一种 成员指针的功能,即Pointer-to-member operator .*和->*,这已经够用了。 而且,从函数类型完整性上也可以理解,C语言函数形式的函数类型是完整的,可以指向某个具体的函数实体, 因此对它添加指针或引用是有意义的,而成员函数形式的函数类型是不完整的,缺乏母类信息,不能指向某个 具体的成员函数,对它添加指针或引用是没有意义的,因此C++选择没有增加不必要的复杂度。 如果把尾置的引用当成函数类型的一部分,不把它理解成常规引用(某种类型整体的引用),而add/remove reference针对的是常规引用,那么也可以理解为: 成员函数形式的函数类型不能添加引用(常规引用)。 add pointer and remove pointer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // C语言函数形式,能添加指针 CETYPE(std::add_pointer_t<void(int)>); // void (*)(int) CETYPE(std::add_pointer_t<void(int,...)>); // void (*)(int, ...) CETYPE(std::add_pointer_t<void(int,...) noexcept>); // void (*)(int, ...) noexcept // C语言函数指针类型,能去除指针 CETYPE(std::remove_pointer_t<void (*)(int)>); // void (int) CETYPE(std::remove_pointer_t<void (*)(int,...)>); // void (int, ...) CETYPE(std::remove_pointer_t<void (*)(int,...) noexcept>); // void (int, ...) noexcept // 成员函数形式,不能添加指针 CETYPE(std::add_pointer_t<void(int) const>); // void (int) const CETYPE(std::add_pointer_t<void(int,...) &>); // void (int, ...) & CETYPE(std::add_pointer_t<void(int) volatile &&>); // void (int) volatile && 可见: C语言函数形式的函数类型可以添加指针,而成员函数形式的函数类型不能添加指针; C语言函数形式的函数类型是完整的函数类型,但成员函数形式的函数类型是不完整的, 因为它不具有包含它的母类的类型信息,所以不难理解它不能被添加指针。 实际上,如果强制为其添加指针,会导致编译报错,std::add_pointer_t里面规避了这点, 对于不能添加指针的类型,返回了输入类型本身。 add const/volatile, remove const/volatile 1 2 3 4 5 6 7 8 9 10 11 12 // C语言函数形式,不能添加cv CETYPE(std::add_const_t<void(int)>); // void (int) CETYPE(std::add_volatile_t<void(int,...)>); // void (int, ...) CETYPE(std::add_cv_t<void(int,...) noexcept>); // void (int, ...) noexcept // 成员函数形式,不能更改原有cv CETYPE(std::add_const_t<void(int) & noexcept>); // void (int) & noexcept CETYPE(std::add_volatile_t<void(int,...) const>); // void (int, ...) const CETYPE(std::add_cv_t<void(int) volatile &&>); // void (int) volatile && CETYPE(std::remove_cv_t<void(int) const>); // void (int) const CETYPE(std::remove_cv_t<void(int,...) volatile &>); // void (int, ...) volatile & CETYPE(std::remove_cv_t<void(int) const && noexcept>); // void (int) const && noexcept 可见: C语言函数形式的函数类型不能添加cv属性,而成员函数形式的函数类型本身自带的cv属性也不能被更改。 如果把尾置的cv属性当成函数类型的一部分,而add const/volatile, remove const/volatile 指的是针对某个类型整体之外添加或去除cv属性,那么也可以理解为: 成员函数形式的函数类型也不能添加cv属性。 也就是,所有函数类型都不能添加cv属性。 is_reference, is_const, is_volatile 对函数类型进行 std::is_const, std::is_references 判断: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 std::cout << std::boolalpha; // 成员函数形式,这些都输出 false std::cout << std::is_reference_v<void(int) &> << std::endl; std::cout << std::is_reference_v<void(int) &&> << std::endl; std::cout << std::is_reference_v<void(int) const &> << std::endl; std::cout << std::is_const_v<void(int) const > << std::endl; std::cout << std::is_const_v<void(int) const &> << std::endl; std::cout << std::is_volatile_v<void(int) volatile > << std::endl; std::cout << std::is_reference_v<void(int)> << std::endl; std::cout << std::is_const_v<void(int)> << std::endl; std::cout << std::is_volatile_v<void(int)> << std::endl; // C语言函数形式,这些都输出 true std::cout << std::is_reference_v<void (&)(int) > << std::endl; std::cout << std::is_reference_v<void (&&)(int)> << std::endl; std::cout << std::is_reference_v<void (&)(int) noexcept> << std::endl; 可见当const、volatile和引用等属性成为函数签名即成为函数类型的一部分的时候,这种属性就不能用 std::is_reference, std::is_const, std::is_volatile判断出来了,结果永远是false。 而C语言函数形式的函数类型的引用不属于函数签名的一部分,可以通过std::is_reference判断出来。 小结 由以上可得,对函数类型进行 add/remove const/volatile/reference 等操作时, 如果某种属性是属于函数签名的一部分,那么这种属性就是这个基本类型的一部分,它是不会被改变的。 而且,当某种属性属于函数签名的一部分时,是无法通过 is const/volatile/reference 等判断出是否具有这种属性的。 可以这样理解:把函数签名整体当成一个黑盒,黑盒内部的函数签名形式上是否具有某种属性是不可见的, 也不可伸入黑盒内部对函数签名进行修改。 函数签名中具有尾置的const、volatile、引用的函数类型是不完整成员函数类型,不能对其添加指针和引用,否则可以。 所有函数类型都不能add const/volatile. 如何判断一个类型是否是 std::function std::function是对函数的封装,它接受一个模板参数,该模版参数应该是一个函数类型。 经过以上讨论,我们知道函数类型有48种形式,那么是否每一种std::function都支持呢? 对于成员函数形式的函数类型我们知道它们是不完整的,对这些函数类型特化没有任何意义,那剩下的4种 C语言函数形式的函数类型 std::function是否都支持呢?事实是,目前(2023)std::function 只支持最基本的那一种,也就是只有这一种特化: 1 2 3 template< class > class function; /* undefined */ template< class R, class... Args > class function<R(Args...)>; // 没有对其他形式特化 验证举例: 1 2 3 4 5 6 7 8 std::function<void(int)> f1; // OK! // 这些都会编译错误:implicit instantiation of undefined template。因为标准库中并没有为其特化。 // std::function<void(int, ...)> f2; // std::function<void(int) noexcept> f3 // std::function<void(int) const> f4; // std::function<void(int) &> f5; // std::function<void(int) &> f6; 上例中f2~f6会编译错误,说明std::function不支持这些函数类型。 当然如果只声明f2~f6对应的那些类型而不用它们定义变量,是不会报错的,但是这没有意义, 这只是声明了一个没有任何实现的类类型而已。 所以目前std::function只支持这一种最基本的函数类型形式, 要想判断一个类型是否是std::function类型,只需要对Return(Args...)这一种函数类型形式做一个特化即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 // 只需这一个primary template和一个特化 template <typename T> struct is_stdfunction : std::false_type {}; template <typename Return, typename... Args> struct is_stdfunction<std::function<Return(Args...)>> : std::true_type {}; // 以下这些特化都是不需要的,因为std::function的实现中并没有为其特化 // template <typename Return, typename... Args> // struct is_stdfunction<std::function<Return(Args..., ...)>> : std::true_type {}; // template <typename Return, typename... Args> // struct is_stdfunction<std::function<Return(Args...) noexcept>> : std::true_type {}; // template <typename Return, typename... Args> // struct is_stdfunction<std::function<Return(Args..., ...) noexcept>> : std::true_type {}; std::function如何处理可变长实参(variadic functions) 此外,还需要说明的一个特例是可变长实参,std::function可以说是不支持这种函数类型,但也可以说是 不完全支持,或部分支持。 std::function的特化中虽然不支持variadic这种函数类型形式, 但是事实上我们可以将带有可变长实参的函数当成没有可变长实参的函数来用, 然后再封装在std::function里面,不过这样虽然可行,但是却丢了可变长实参部分的功能。 例如: 1 2 3 4 // std::function<int(const char*, ...)> f1 = printf; // error: implicit instantiation of undefined template 'std::function<int (const char *, ...)>' std::function<int(const char*)> f2 = printf; // OK f2("printf no more arguments"); // OK // f2("printf %d", 1); // error: no matching function for call to object of type 'std::function<int (const char *)>' 也许这也没有多大实用性,所以严格来说,std::function对带有可变长实参的函数类型还是不支持的。 那么应不应该支持一下呢?未来std::function会不会对带有variadic和noexcept的函数类型做一个特化呢? 如果他想,其实是可以的,那时候我们的is_stdfunction的后三种特化就得用上了。 不过,对noexcept的特化其实是不必要的,因为带有noexcept的函数指针,是可以转化成对应不带noexcept的函数指针的。 例如: 1 2 3 4 5 6 7 8 9 10 11 12 void f1(int) {} void f2(int) noexcept {} int main() { static_cast<decltype(&f1)>(&f2); // OK // static_cast<decltype(&f2)>(&f1); // error auto p1 = &f1; p1 = &f2; // OK auto p2 = &f2; // p2 = &f1; // error } 例中也看到了,反过来,不带有noexcept的函数指针是不能转化成对应的带有noexcept的函数指针的。 这也可以理解,毕竟noexcept是更严格的限制,可是说是子集,而不带noexcept的是超集。 再有,针对可变长实参这个属性呢? 当然,有可变长实参的函数指针与对应的没有可变长实参函数指针之间是不能相互转化的,例如: 1 2 3 4 5 6 7 8 9 10 11 12 void f1(int) {} void f2(int, ...) {} int main() { // static_cast<decltype(&f1)>(&f2); // error // static_cast<decltype(&f2)>(&f1); // error auto p1 = &f1; // p1 = &f2; // error auto p2 = &f2; // p2 = &f1; // error } 这个std::function不对其特化,可能就是不想支持这种函数类型了吧,毕竟这是C语言遗留产物。 如何判断一个类型是否是 lambda lambda是C++中的匿名类型,每个新定义的lambda都具有各自不同的类型,一般来说我们无法判断一个类型是否是lambda。 但是有一种特殊的lambda,即捕获列表为空的(captureless)且不是generic的 (使用auto声明参数类型的叫作generic lambda),这种特殊的lambda有一个独有的性质, 就是它能够转换成与它的operator()对应的C语言风格的函数指针,并且指向它的operator()。 我们可以判断一个类型是否具有这种性质,假如具有,那么我们可以猜测这种类型大概率可能就是 这种特殊的lambda,除非用户手动定义了一个具有这种性质的自定义类型,而通常很少有人这么做,也不该这么做。 所以虽然我们无法判断一个类型是否是lambda,但我们可以判断一个类型有可能是 captureless并且non-generic的lambda。判断方法如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 // Get a C function pointer type by a member function pointer type template <typename T> struct detect_cfunction { using type = void; }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args...)> { using type = Return (*)(Args...); }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args...) const> { using type = Return (*)(Args...); }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args..., ...)> { using type = Return (*)(Args..., ...); }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args..., ...) const> { using type = Return (*)(Args..., ...); }; #if __cplusplus >= 201703 template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args...) noexcept> { using type = Return (*)(Args...) noexcept; }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args...) const noexcept> { using type = Return (*)(Args...) noexcept; }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args..., ...) noexcept> { using type = Return (*)(Args..., ...) noexcept; }; template <typename Object, typename Return, typename... Args> struct detect_cfunction<Return (Object::*)(Args..., ...) const noexcept> { using type = Return (*)(Args..., ...) noexcept; }; #endif template <typename T> using detect_cfunction_t = typename detect_cfunction<T>::type; // Detect type of callee, which is a pointer to member function operator() template <typename T, typename = void> struct detect_callee { using type = void; }; #if __cplusplus < 201703 namespace std { template <typename T> using void_t = void; } #endif template <typename T> struct detect_callee<T, std::void_t<decltype(&T::operator())>> { using type = decltype(&T::operator()); }; template <typename T> using detect_callee_t = typename detect_callee<T>::type; // Detect C function style callee type template <typename T> using detect_c_callee_t = detect_cfunction_t<detect_callee_t<T>>; // Detect whether T maybe a captureless and non-generic lambda template <typename T> struct maybe_lambda : std::integral_constant< bool, !std::is_same<T, void>::value && std::is_convertible<T, detect_c_callee_t<T>>::value> {}; 举例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #define TEST(x) std::cout << #x << ": " << maybe_lambda<x>::value << std::endl; struct A {}; struct B { void operator()() {} }; struct C { void operator()() const volatile & {} }; int main() { std::cout << std::boolalpha; auto l1 = [](int){}; TEST(decltype(l1)); auto l2 = [](int, ...){}; TEST(decltype(l2)); const auto l3 = [](int){}; TEST(decltype(l3)); const auto & l4 = [](int){}; TEST(decltype(l4)); TEST(std::decay_t<decltype(l4)>); auto && l5 = [](int){}; TEST(decltype(l5)); TEST(std::decay_t<decltype(l5)>); auto l6 = [](auto){}; // generic lambda TEST(decltype(l6)); auto l7 = [&](int){}; TEST(decltype(l7)); auto l8 = [=](int){}; TEST(decltype(l8)); auto l9 = [](int) noexcept {}; TEST(decltype(l9)); auto l10 = [](int, ...) noexcept {}; TEST(decltype(l10)); auto l11 = [](auto) noexcept {}; TEST(decltype(l11)); auto l12 = [=](int) noexcept {}; TEST(decltype(l12)); TEST(A); TEST(B); TEST(B&); TEST(C); TEST(C&&); TEST(std::function<void(int)>); TEST(std::function<void(int)>&); TEST(int(int)); TEST(void(void)); TEST(void(int, ...)); TEST(int(&)(int)); TEST(int(*)(int)); TEST(void(*&)(int)); TEST(int); TEST(void); TEST(int*); TEST(void*); return 0; } 输出结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 decltype(l1): true decltype(l2): true decltype(l3): true decltype(l4): false std::decay_t<decltype(l4)>: true decltype(l5): false std::decay_t<decltype(l5)>: true decltype(l6): false decltype(l7): false decltype(l8): false decltype(l9): true decltype(l10): true decltype(l11): false decltype(l12): false A: false B: false B&: false C: false C&&: false std::function<void(int)>: false std::function<void(int)>&: false int(int): false void(void): false void(int, ...): false int(&)(int): false int(*)(int): false void(*&)(int): false int: false void: false int*: false void*: false 如何判断一个类型是否是可调用的 可调用基本类型(这里不包括其引用)分两种,一种是具有operator()的类类型,另一种是C语言函数类型及其指针。 1. 判断一个类型是否具有operator()的类类型 operator()分两种,一种是非模版函数,一种是模版函数。 对于operator()不是模版函数的类型来说,可以用如下方法判断: 1 2 3 4 5 6 // Whether T has a non-template member operator() template <typename T, typename = void> struct is_callable_class : std::false_type {}; template <typename T> struct is_callable_class<T, std::void_t<decltype(&T::operator())>> : std::true_type {}; 对于operator()是模版函数的类,目前还没找到判断方法。 举例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #define TEST(x) std::cout << #x << ": " << is_callable_class<x>::value << std::endl; struct A {}; struct B { void operator()() {} }; struct C { void operator()() const volatile & {} }; int main() { std::cout << std::boolalpha; auto l1 = [](int){}; TEST(decltype(l1)); auto l2 = [](int, ...){}; TEST(decltype(l2)); const auto l3 = [](int){}; TEST(decltype(l3)); const auto & l4 = [](int){}; TEST(decltype(l4)); TEST(std::decay_t<decltype(l4)>); auto && l5 = [](int){}; TEST(decltype(l5)); TEST(std::decay_t<decltype(l5)>); auto l6 = [](auto){}; // generic lambda TEST(decltype(l6)); auto l7 = [&](int){}; TEST(decltype(l7)); auto l8 = [=](int){}; TEST(decltype(l8)); TEST(A); TEST(B); TEST(B&); TEST(C); TEST(C&&); TEST(std::function<void(int)>); TEST(std::function<void(int)>&); TEST(int(int)); TEST(void(void)); TEST(void(int, ...)); TEST(int(&)(int)); TEST(int(*)(int)); TEST(void(*&)(int)); TEST(int); TEST(void); TEST(int*); TEST(void*); return 0; } 输出结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 decltype(l1): true decltype(l2): true decltype(l3): true decltype(l4): false std::decay_t<decltype(l4)>: true decltype(l5): false std::decay_t<decltype(l5)>: true decltype(l6): false decltype(l7): true decltype(l8): true A: false B: true B&: false C: true C&&: false std::function<void(int)>: true std::function<void(int)>&: false int(int): false void(void): false void(int, ...): false int(&)(int): false int(*)(int): false void(*&)(int): false int: false void: false int*: false void*: false 2. 判断一个类型是否是C语言函数类型或其指针类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // whether T is C function template <typename T> struct is_cfunction : std::false_type {}; template <typename Return, typename... Args> struct is_cfunction<Return(Args...)> : std::true_type {}; template <typename Return, typename... Args> struct is_cfunction<Return(Args..., ...)> : std::true_type {}; #if __cplusplus >= 201703 template <typename Return, typename... Args> struct is_cfunction<Return(Args...) noexcept> : std::true_type {}; template <typename Return, typename... Args> struct is_cfunction<Return(Args..., ...) noexcept> : std::true_type {}; #endif // whether T is C function pointer template <typename T> struct is_cfunction_pointer : std::integral_constant< bool, std::is_pointer<T>::value && is_cfunction<std::remove_pointer_t<T>>::value> {}; // Whether T is C style callable: C function or C function pointer template <typename T> struct is_callable_c : std::integral_constant< bool, is_cfunction<T>::value || is_cfunction_pointer<T>::value> {}; 举例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #define TEST(x) std::cout << #x << ": " << is_callable_c<x>::value << std::endl; struct A {}; struct B { void operator()() {} }; struct C { void operator()() const volatile & {} }; int main() { std::cout << std::boolalpha; auto l1 = [](int){}; TEST(decltype(l1)); auto l2 = [](int, ...){}; TEST(decltype(l2)); const auto l3 = [](int){}; TEST(decltype(l3)); const auto & l4 = [](int){}; TEST(decltype(l4)); TEST(std::decay_t<decltype(l4)>); auto && l5 = [](int){}; TEST(decltype(l5)); TEST(std::decay_t<decltype(l5)>); auto l6 = [](auto){}; // generic lambda TEST(decltype(l6)); auto l7 = [&](int){}; TEST(decltype(l7)); auto l8 = [=](int){}; TEST(decltype(l8)); TEST(A); TEST(B); TEST(B&); TEST(C); TEST(C&&); TEST(std::function<void(int)>); TEST(std::function<void(int)>&); TEST(int(int)); TEST(void(void)); TEST(void(int, ...)); TEST(int(&)(int)); TEST(int(*)(int)); TEST(void(*&)(int)); TEST(int); TEST(void); TEST(int*); TEST(void*); return 0; } 输出如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 decltype(l1): false decltype(l2): false decltype(l3): false decltype(l4): false std::decay_t<decltype(l4)>: false decltype(l5): false std::decay_t<decltype(l5)>: false decltype(l6): false decltype(l7): false decltype(l8): false A: false B: false B&: false C: false C&&: false std::function<void(int)>: false std::function<void(int)>&: false int(int): true void(void): true void(int, ...): true int(&)(int): false int(*)(int): true void(*&)(int): false int: false void: false int*: false void*: false 判断一个类型是否是可调用的 可调用类型就是以上两种情况的综合,is_callable_class或is_callable_c。 另外,如果想包括其引用也判断为可调用类型的话,可以先将某类型decay,然后再判断是否是以上两种情况之一。 1 2 3 4 5 6 7 8 // whether T is callable template <typename T> struct is_callable : std::integral_constant< bool, is_callable_class<T>::value || is_callable_c<T>::value> {}; // whether decay_t<T> is callable template <typename T> struct decay_is_callable : is_callable<std::decay_t<T>> {}; 举例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #define TEST(x) std::cout << #x << ": " << is_callable<x>::value << std::endl; struct A {}; struct B { void operator()() {} }; struct C { void operator()() const volatile & {} }; int main() { std::cout << std::boolalpha; auto l1 = [](int){}; TEST(decltype(l1)); auto l2 = [](int, ...){}; TEST(decltype(l2)); const auto l3 = [](int){}; TEST(decltype(l3)); const auto & l4 = [](int){}; TEST(decltype(l4)); TEST(std::decay_t<decltype(l4)>); auto && l5 = [](int){}; TEST(decltype(l5)); TEST(std::decay_t<decltype(l5)>); auto l6 = [](auto){}; // generic lambda TEST(decltype(l6)); auto l7 = [&](int){}; TEST(decltype(l7)); auto l8 = [=](int){}; TEST(decltype(l8)); TEST(A); TEST(B); TEST(B&); TEST(C); TEST(C&&); TEST(std::function<void(int)>); TEST(std::function<void(int)>&); TEST(int(int)); TEST(void(void)); TEST(void(int, ...)); TEST(int(&)(int)); TEST(int(*)(int)); TEST(void(*&)(int)); TEST(int); TEST(void); TEST(int*); TEST(void*); return 0; } 输出结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 decltype(l1): true decltype(l2): true decltype(l3): true decltype(l4): false std::decay_t<decltype(l4)>: true decltype(l5): false std::decay_t<decltype(l5)>: true decltype(l6): false decltype(l7): true decltype(l8): true A: false B: true B&: false C: true C&&: false std::function<void(int)>: true std::function<void(int)>&: false int(int): true void(void): true void(int, ...): true int(&)(int): false int(*)(int): true void(*&)(int): false int: false void: false int*: false void*: false 测试decay_is_callable结果如下: 1 2 #define TEST(x) std::cout << #x << ": " << decay_is_callable<x>::value << std::endl; // 代码同上,这里省略 输出: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 decltype(l1): true decltype(l2): true decltype(l3): true decltype(l4): true std::decay_t<decltype(l4)>: true decltype(l5): true std::decay_t<decltype(l5)>: true decltype(l6): false decltype(l7): true decltype(l8): true A: false B: true B&: true C: true C&&: true std::function<void(int)>: true std::function<void(int)>&: true int(int): true void(void): true void(int, ...): true int(&)(int): true int(*)(int): true void(*&)(int): true int: false void: false int*: false void*: false 成员指针类型 用子类取基类成员的地址得到什么类型? 例如: 1 2 3 4 5 6 7 8 9 struct B { int i = 1; }; struct D: public B { }; int main() { static_assert(std::is_same<decltype(&D::i), int B::*>::value, "Never happen"); static_assert(std::is_same<decltype(&D::B::i), int B::*>::value, "Never happen"); } 可见,用子类取基类成员的地址得到的是基类成员指针类型。如上例中 &D::i 就相当于 &D::B::i, 得到的成员指针类型是int B::*。 成员指针类型书写形式中的类类型,如果是 cv-qualified 会有什么影响? 例如: 1 2 3 4 5 6 7 8 9 10 11 struct B { int i = 1; }; using ConstB = const B; int main() { static_assert(std::is_same<int B::*, int ConstB::*>::value, "Never happen"); static_assert(std::is_same<int B::*, int std::add_const_t<B>::*>::value, "Never happen"); static_assert(std::is_same<int B::*, int std::add_volatile_t<B>::*>::value, "Never happen"); static_assert(std::is_same<int B::*, int std::add_cv_t<B>::*>::value, "Never happen"); } 可见,成员指针类型书写形式中的类类型如果是 cv-qualified,那么C++会自动将其remove_cv,也就是 视作与相应的 成员指针类型书写形式中类类型是 cv-unqualified 的类型 是同一种类型。 并不会像给类类型加引用那样编译报错,例如int std::add_lvalue_reference_t<B>::*会编译报错 “type ‘add_lvalue_reference_t<B>’ (aka ‘B &’) cannot be used prior to ‘::’ because it has no members”。

2023/5/16
articleCard.readMore

排序融合公式 | Ranking Value Model

考虑有限候选多目标融合排序公式,目标个数$T$,候选对象个数$N$。 每一个候选对象都有$T$个目标分,需要按某个融合公式把它们融合成一个最终用于排序的分数。 加法融合公式 加法融合公式是线性的,形式简单,参数少,调参容易。其形式是: $$ Score = \sum_{i=1}^T w_i \cdot (b_i + t_i) $$ 其中,$t_i$ 为第$i$个目标的得分,$b_i$ 为第$i$个目标的Bias,一般固定为0,$w_i$ 为第$i$个目标的权重。 乘法融合公式 乘法融合公式是非线性的,相对加法融合公式来说形式复杂,参数多,调参稍微困难一些,但一般能比加法融合公式取得更好的效果。 其基础形式是: $$ Score = \prod_{i=1}^T (\beta_i + t_i) ^ {\alpha_i} $$ 其中,$t_i$ 为第$i$个目标的得分,$\alpha_i$和$\beta_i$ 为调整第$i$个目标权重的参数。实践中为了便于调参,可以把它改写成Bias形式: $$ Score = \prod_{i=1}^T (b_i + \beta_i \cdot t_i) ^ {\alpha_i} $$ $b_i$ 为第$i$个目标的Bias,一般固定为1,然后一个简单的调参方式是:在$t_i$的平均值的倒数的基础上调节$\beta_i$,在1附近调节$\alpha_i$。 目标分预处理 除了将目标分直接代入融合公式外,还可以先对其做一定的预处理。 归一化(Normalization) 目标分分布不稳定或分布不便于直接用到排序融合公式中时,可以把目标分归一化线性映射到稳定的$[A, B]$的区间,一般$B$取1,$A$可取0.01或更小或0: $$ \tilde{t} = \frac{ t - \min t } { \max t - \min t} \cdot (B - A) + A $$ 其中, $\max t = \max \limits_{1 \le j \le N} t^{(j)}$ 为目标分在$N$个候选对象上的分布的最大值, $\min t = \min \limits_{1 \le j \le N} t^{(j)}$ 为目标分在$N$个候选对象上的分布的最小值, $t^{(j)}$ 表示第$j$个候选对象的目标分。 要注意,实践中要规避除零的风险。 相对目标分 在推荐系统中,不同用户对某一个动作的喜好程度或使用频率不同,则表现为某一个目标分在不同用户上的分布有大小区别, 因此可以用该目标分在当前待排序的有限候选集上的相对得分取代原始得分: $$ \hat{t} = \frac {t} { w \cdot \overline{t} + c } $$ 其中, $\overline{t} = \frac { \sum_{j=1}^N t^{(j)} } {N}$ 为目标分在$N$个候选对象上的分布的平均值, $w$和$c$为待调整的参数。 例如,应用相对目标分的乘法融合公式为: $$ Score = \prod_{i=1}^T (b_i + \frac {t_i} {w_i \cdot \overline{t}_i + c_i} ) ^ {\alpha_i} $$ 此式也被称为个性化$\beta$形式,因为不同用户的目标分平均值$\overline{t}_i$不同,也就相当于Bias形式乘法融合公式中的$\beta_i$是不同的。 其他 此外,还可以将目标分按大小阈值截断,使用Sigmoid函数映射到0~1等。 在融合公式中使用目标排名 除了在融合公式中使用目标得分外,还可以使用目标排名代替目标分。 对全体候选对象按某一个目标分排序,则可得到每一个候选对象在这一个目标上的排名,记为$r_i$,表示在第$i$个目标上的排名,值为$[1, N]$之间的整数。 然后可以直接在融合公式中使用目标排名,或者将其归一化到$[\frac {1} {N}, 1]$之间,也就是将$r_i$除以$N$,之后再代入融合公式。 需要注意的是,由于排序时一般是逆序排序,目标分最大的排名是1,因此使用目标排名代替目标分时改变了融合公式的单调性,需要相应地再调整一下融合公式的单调性。 例如,在乘法融合公式中使用目标排名时,可以用$-\alpha$作为指数: $$ Score = \prod_{i=1}^T (b_i + \beta_i \cdot \tilde{r}_i) ^ {- \alpha_i} $$ 其中,$\tilde{r}_i = \frac {r_i} {N}$。 或者,可以用$\tilde{r}_i = \frac {N + 1 - r_i} {N}$的映射方式对排名数值进行归一化, 则不需要再调整融合公式的单调性。

2022/6/11
articleCard.readMore

Resume

技能值 Basics: C++(0.85), Python(0.75), Golang(0.6), Lua(0.6) Data Structures and Algorithms(0.8), Networks(0.6), Operating System(0.6) ML(0.4) Applications: Comment Ranking System(0.9), Recommendation System(0.6) Tools and Frameworks: Git, Redis, Nsq, Kafka, Thrift 工作经历 2017-2022,北京,字节跳动,后端开发工程师。评论排序系统架构和策略,推荐系统架构和策略。 字节跳动 2017.7~2022.6 1. 评论排序系统架构和排序功能策略开发负责人 全权负责字节跳动评论中台评论排序服务架构开发、优化和排序业务功能以及排序策略开发。 经历评论浏览峰值从10万QPS到100万QPS量级增长期间的在线服务架构持续优化。稳定性几乎持续保持在三个九以上,数年来保持自己一个人运维无问题。 经历评论日发表量从千万到几亿量级增长期间离线评论排序索引更新服务的架构持续优化。服务性能、更新可靠性、实时性、对复杂多样业务需求的支持能力持续提高。 经历从以今日头条App为核心的简单评论排序功能到支持字节跳动全部产品线数十个App的评论排序功能需求开发。业务需求复杂多变,对评论排序功能和策略的多样性、灵活性、实时性等的要求持续提高,数年来自己一人承担开发无问题。 亮点项目:Maglev一致性哈希下的负载均衡,从无状态服务到有状态服务。 上游Proxy负责请求分发,在保持负载均衡的前提下尽量保持一致性哈希,使得后端服务节点LocalCache命中率大幅度提高,大幅提高服务器内存资源使用效率、降低服务器CPU资源使用量、降低服务响应延时、降低网络带宽占用。同时Proxy实时统计监控每一个下游节点健康状态,能应对热Key问题,支持故障节点自动下线、故障恢复后自动上线。 (产出博文以及开源代码:https://lishuangquan.cn/post/2022/maglev-consistent-hash) 亮点项目:评论排序策略的动态配置化设计,在线服务的参数管理思想。 大幅提高评论排序策略代码的通用性和可配置性。轻松以一个服务应对上游数十个业务产品线不同的评论排序策略。简单的或已经在其他产品线开发过的功能可通过远程配置快速应用到某个有同样需求的产品线或其中某个具体应用场景,不必修改代码上线。 (以此为启发,开发了一个可将Lua脚本嵌入到C++项目中的开源项目:https://github.com/peacalm/cpp-luaw) 长尾请求排查优化,通过异步RPC请求精准控制RPC延时提高服务可用性。 高延时排查优化,通过给LocalCache分桶使评论排序服务延时从120ms降低到30ms。 2. 推荐系统架构和策略工作 参与过抖音社交推荐服务架构优化与策略开发,与推荐算法同学对接。参与过TikTok视频推荐评论目标优化工作,有过模型训练经历。对字节跳动推荐系统架构有一定了解。 除了开发具体琐碎的业务功能外,产出过通用解决方案得到推荐组内多数人使用,包括:ValueTree改进:定义变量默认值,避免未定义错误;用浏览侧实验层开作者侧实验的通用方法等。 个人开源项目 cpp-luaw:在C++项目中使用Lua脚本的工具库。——2023~2024 地址:https://github.com/peacalm/cpp-luaw 支持在C++中读取Lua中的值、将C++中的值传递给Lua、在C++中调用Lua中的函数、将C++中定义的函数绑定给Lua调用、将C++中定义的类包括其成员变量和成员函数绑定给Lua使用、在C++中对Lua表达式进行求值等。 cpp-maglev:基于Maglev一致性哈希算法的动态负载均衡库。——2022 地址:https://github.com/peacalm/cpp-maglev (工作经历中已详述,另见博文:https://lishuangquan.cn/post/2022/maglev-consistent-hash) C++基础算法代码库。(学生时期算法学习和C++编程学习成果)——2016 地址:https://github.com/peacalm/Cplusplus-Algorithms-Library 包括高精度整数类型,树状数组类,位操作算法库,数论和组合数学算法库,矩阵类,并查集类,回文树类,排序算法库,string类扩展功能库,touchable_heap容器类以及各种容器类型的ostream输出支持和实用调试工具等。 (2025)专业技能 熟练掌握C++、Python,熟悉Golang、Lua。扎实的数据结构和基础算法能力。 热爱编程,良好的代码规范和文档习惯。喜欢编写高质量、通用的、可分享复用的代码库。喜欢C++泛型编程,GitHub上有个人开源项目。 具有高吞吐、高并发、高性能、高可用的微服务开发和优化经验。 深入理解评论排序服务,有百万QPS级评论排序服务全链路开发优化经验,具备从0到1搭建能力。 熟悉推荐系统架构。对机器学习和推荐算法等稍有了解。 知识结构包括数据结构及常用算法、计算机网络、操作系统、数据库以及系统测试等。 教育经历 时间 学校 院系 专业 学位 2014/09 – 2017/06 东南大学 自动化学院 模式识别与智能系统 工学硕士 2010/09 – 2014/06 东南大学 自动化学院 自动化 工学学士 (在校时期)专业技能 熟练掌握C/C++语言,能够快速运用C/C++进行软件开发,爱好泛型编程; 熟练掌握Python语言编程。 熟练掌握数据结构和常用算法设计,喜欢编程比赛,在Github上创建并维护一个开源算法库。 熟悉Linux系统操作及Bash脚本编程,熟悉Unix/Linux环境编程。 熟悉嵌入式Linux系统开发流程,掌握MCU、DSP、ARM等处理器的硬件系统设计与软件开发; 知识结构包括数据结构及常用算法、计算机网络、操作系统、数据库以及系统测试等。 (在校时期)获奖情况 时间 奖项 名次 级别 2016 计蒜之道2016程序设计大赛 复赛前400 企业级 2016 思杰代码大师 二等奖(第二名) 企业级 2016 白山极客挑战赛 铜奖(138/825) 企业级 2016 华为2016软件精英挑战赛 江山赛区二等奖 企业级 2016 Calix 2016 Future Star编程大赛 三等奖 企业级 2015 全国研究生数学建模大赛 二等奖 国家级 2015 东南大学自动化学院研究生会先进个人 院级 2014 全国研究生数学建模竞赛 三等奖 国家级 2014 东南大学自动化学院本科生优秀毕业设计 院级 2013 东南大学电子设计竞赛 一等奖 校级 2013 东南大学智能车竞赛 优秀奖 校级 2012 东南大学Robcup机器人竞赛3D仿真组 三等奖 校级 (在校时期)项目经历 Cplusplus Imporvement Library C++开源算法库(https://github.com/peacalm/Cplusplus-Improvement-Library) 电梯安全检测仪系统设计 基于MCU的血液凝固检测仪系统设计和实现 本科毕设。

2022/6/11
articleCard.readMore

Maglev一致性哈希和动态负载均衡 | Maglev Consistent Hasher and Dynamic Load Balancer

本文重点描述Maglev一致性哈希算法,并提出使Maglev一致性哈希算法支持带权重候选节点的改进方式, 以及描述了一致性哈希下的动态负载均衡策略,并给出开源C++实现代码库。 一致性哈希 一致性哈希是一种将属于无限集的key稳定地映射到属于有限集的候选节点上的算法,它需要满足: 稳定:候选节点集合不变时,一个固定的key,会稳定不变地映射到某一个候选节点上; 最小扰动:当增加或减少候选节点时,只有少部分key需要重新映射,大部分key的映射结果不变; 均衡:不同key应该均匀地分散到各个候选节点上,即一个key映射到每一个候选节点上的概率是相等的。 常见一致性哈希算法 环形哈希,也叫割环法,经典的一致性哈希算法,作者Karger等人于1997年提出一致性哈希算法的概念, 然后提出了这个一致性哈希算法。 更新和查询时间复杂度都是O(log(n)),空间复杂度O(n), 但是通常均衡性不好,需要加入较多虚拟节点,也就加倍了时间和空间复杂度。 Jump Consistent Hash。极简的一致性哈希算法,不到十行代码。 查询时间复杂度是O(log(n)),不需要更新操作,也不需要额外存储空间。 但是不能随机增删候选节点,只能在有序候选节点队列的尾部增删节点,实用性不强。 Maglev一致性哈希算法。 查询时间复杂度O(1),更新时间复杂度O(m),空间复杂度O(m),m通常为至少比n大10倍以上的一个素数常量。 较为实用,效果较好,下面重点介绍。 Maglev一致性哈希算法描述 选取哈希槽长度,素数M,生成空哈希槽数组; 将候选节点列表排序; 对每一个候选节点哈希得到两个随机数A、B(模M-1再加1保证非0或M的倍数),然后得到一个从0到M-1的排列:X[k] = (A + k*B)%M, k=0,1,…,M-1; 排列中每一个数字代表一个槽位,轮询每一个候选节点的排列,从左到右选择排列中的第一个空槽位填充进去,直到哈希槽填充完整为止; 对一个输入key,通过 key%M 映射到哈希槽中对应的候选节点。(这里的key通常需要哈希一下得到一个大随机数) 优缺点: 优点:分片均匀,均衡性好,查询速度快O(1) 缺点:增删节点后更新较慢O(n),并且没有完全实现最小扰动。 一致性哈希支持带权重候选节点 通用做法:增加虚拟节点 通常一致性哈希算法都是不支持带权重候选节点的,也就是一个key映射到每一个候选节点的概率是相等的。 因此,想要实现带权重的一致性哈希的一个普遍思路是增加虚拟节点。将一个实际的候选节点拆成多个虚拟节点, 拆成的虚拟节点的多少,即代表了这个实际候选节点的权重的大小。 Maglev一致性哈希算法支持带权重候选节点的特殊做法:按权重正比概率阈值填充哈希槽位。 通常,增加虚拟节点的做法相当于增加了候选节点数n,如果时间空间复杂度与n有关,那么会相应增加复杂度。 其次,如果虚拟节点数增加的少,那么实际的权重比例会比较粗糙,即精度不够。 Maglev算法的查询时间复杂度与n无关,是O(1),所以增加虚拟节点法不会影响Maglev的查询速度, 但是由于Maglev算法需要选取一个比候选节点数大很多的大素数M,且这个M关系到更新的时间复杂度和占用的空间复杂度, 因此采用增加虚拟节点法也会增加一些消耗。 观察Maglev算法哈希槽的填充过程可知,该算法是轮训每一个候选节点,让每一个候选节点占有一个哈希槽后才轮到下一个候选节点。 因此可以试想,只要让轮训到当前候选节点时,不一定完全占有一个候选节点,而是设定一个与该节点的权重成正比的概率阈值, 达到这个阈值后才占有一个哈希槽。这个概率阈值可以是:当前节点的权重除以所有节点的权重的最大值。 可以看出,无权重的情况,也就相当于每一个候选节点的权重都相等,因此对应的概率阈值也都相等,都是1。 另外,为了保证一致性哈希算法的稳定性,这里的概率生成要用稳定的伪随机概率,每一个候选节点用自己的固定信息, 比如节点ID,作为一个伪随机序列的种子,用这个伪随机序列称重的概率与对应的概率阈值相比较,来判断该节点在这一次轮训中要不要占有一个哈希槽位。 一致性哈希下的动态负载均衡 由开头对一致性哈希算法的描述中可以,输入的key是属于无限集的,是无法提前预知的,无法对其做任何分布性的要求。 因此,极有可能,输入的key就是极其不均衡的,而纯粹的一致性哈希又是要求结果必须是稳定的,所以不均衡的输入集, 最终会造成不均衡的映射结果。比如常见的热key问题,纯一致性哈希是无法解决的。 因此,为了在未知的不确定的任意的输入集上都保持良好的均衡性,需要动态的调整映射策略,需要统计感知每一个候选节点当前的负载情况, 如果负载过高,则应该将当前key用某种算法重新映射到另外一个节点上,而这个重新映射的算法最好也是稳定的。 对于Maglev算法来说,笔者提出一个简单的重新映射算法:(key + (key % C + 1) * retry_cnt) % M。 其中C是某一个常数,而retry_cnt是重新映射次数,可见当retry_cnt为0时,即首次映射时,该算法就退化到了Maglev算法描述第5步描述的 key % M的算法。 除了重新映射即重新选取候选节点外,对于动态负载均衡来说,另一个重点是,如何描述节点负载以及如何判断是否负载过高。 对于候选节点无权重的情况来说,任意一个候选节点每接收一个key,就增加这个key对应的负载值,如果所有的key对应的负载值都一样,即可记为1。 对于候选节点有权重的情况来说,不同的候选节点接收同一个key后对自身的负载影响是不同,需要乘以一个以自身权重为分母的负载归一化因子, 比如可以是:所有候选节点的权重的平均值除以当前节点的权重。这样,每一个候选节点接收一个key后,需要增加自身的负载归一化因子乘上这个key对应的负载值后, 这么多的负载增量。 有了每一个key对每一个候选节点的负载增量定量描述后,就可以计算所有候选节点的平均负载值,然后设定一个阈值,负载大于平均负载某一个倍数的节点, 即可认为是过载节点。例如,这个倍数可选1.2 ~ 1.5。 特别的,在RPC场景下,候选节点就相当于是远程服务器,每处理一个key就相当于一次远程服务请求。 因此,请求量、错误量、延时等信息也可用来描述候选节点的负载。错误数较多,延时过高时,即可认为当前节点负载过高。 但是为了避免全局的一致性哈希结果被大量破坏,可以对候选节点的每一项负载情况进行排名,然后限制只有该项负载排名很高的几个节点才能在这一负载项上被判定为过载。 最后,负载是动态变化的,负载的统计记录信息需要实时更新,通常需要一个滑动窗口,每隔固定几秒钟,就生成一个新的统计单元,并滑动滑动窗口, 丢弃窗口中最老的统计单元的数据,加入最新的统计单元的数据。 开源实现 以上所描述的Maglev一致性哈希算法,以及在此一致性哈希基础上的动态负载均衡策略,笔者已有一个完整的C++开源实现。 代码详见GitHub:cpp-maglev

2022/5/21
articleCard.readMore

在线服务的异步RPC延时控制和无锁异步任务同步组件

一、关于延时 控制延时是保证在线服务可用性的必要手段 在线服务对延时敏感。一般对在线服务发起远程调用时,都会配有一个超时限制,一旦请求超时, 则认为本次请求失败,服务不可用。因此控制延时是保证在线服务可用性的必要手段。 RPC远程调用耗时的复杂性 网络环境是不可靠的,数据在网络中传输的耗时是不可控的,因此为了控制延时, 需要为每次socket发送和接受数据都配置一个超时限制。 而一次RPC远程调用可能需要执行多次socket请求,例如数据包很大、io状态不佳、需要重试等原因, 因此为单次socket请求设置的超时限制,并不能准确代表一次RPC远程调用的网络耗时。 因此,为了更准确控制超时,一些RPC框架内置了io线程池,采用异步方式进行socket网络请求, 超时控制会比同步模式准确一些。 不过,虽然异步请求是比同步请求模式更先进的控制超时的方式,但更复杂, 也引入了更多影响延时的因素和需要关注和调优的参数。 例如,异步模式虽然可以避免多次socket请求对超时控制的影响,但又引入了 io线程池的调度耗时、控制异步io超时的定时器的准确性等因素对总体RPC延时的影响。 再退一步,从业务客户端视角看,一次RPC远程调用的耗时,除了网络耗时, 还包括数据在本端(客户端)和远端(服务端)的序列化和反序列化耗时,当数据包过大时, 这一部分耗时和对CPU的消耗也是不能忽视的。 异步RPC是必要的 因此,使用异步RPC远程调用总是必要的,除了能使延时控制得更准确外,还可以并行执行多个RPC请求, 进一步降低延时,而并行执行一些相互独立的任务也是降低延时的非常重要的优化策略。 更进一步,除了RPC框架内置的异步网络请求模式外,还可以再退一层,站在业务的更大的视角上看, 在在线服务内建立线程池,用于执行异步任务,这些异步任务不仅可以包括RPC框架涵盖的网络请求, 还可以包含数据的序列化反序列化,以及业务上需要特殊处理的逻辑。 例如,假如一个业务上的异步任务其实内部需要先后访问两次远程服务,然后再做一些处理, 那么用一个业务上的异步任务给它一个整体上的超时,对业务上讲,控制延时将更准确。 或者,业务上的异步任务可以仅仅是对使用了同步RPC框架的一次RPC任务的封装,那么这个业务上设定的超时, 就包含了网络耗时、数据序列化反序列化耗时、业务特殊处理逻辑等所有步骤的耗时, 比单独使用RPC框架的超时,控制延时更准确。 二、关于同步 异步执行的程序之间总是需要同步的。 无锁同步 线程间同步一般常用锁,例如互斥锁、读写锁等,而"无锁"同步,一般就要直接使用原子标记,事实上, 锁的内部实现方式也是使用了原子标记。 异步任务同步 业务上给每一个异步任务都设定一个预期的超时时间,主线程发送出一个或多个异步任务后, 等待一段时间——这个时间应该是这些异步任务超时时间的最大值——让这些任务并行执行一会儿, 然后需要判断异步任务执行得怎么样了,是已经成功了?或是已经失败了?或是还在执行中? 如果还在执行中,那么主线程可以给这个任务标记为"已超时", 让这个超时任务不要再影响主线程,或进入超时任务的特殊处理方法。 异步任务成功或失败的标记信息,是任务线程发送给主线程的信号, 而任务已超时的信息,是主线程发送给任务线程的信号。 这些就涉及到线程间同步问题。 异步任务无锁同步组件,C++为例 以下C++实现代码中 SyncKit类 就是一个简单的异步任务无锁同步组件, 它是主线程和异步任务线程共享的数据结构,分别为其定义了可调用成员方法。 主线程调用master_开头的方法,异步任务线程调用slave_开头的方法。 主线程使用方式很简单,发出异步任务后等待一段时间,然后调用master_check_ret()检查结果即可。 任务线程使用方式稍微复杂,主要要保证任务完成后将结果数据写入主线程提供的承载任务结果的数据结构时, 要保证主线程尚在等待,还没有标记超时。否则,如果主线程已经标记了超时,代表主线程可能已经开始使用 承载任务结果的数据结构了,如果此时任务线程还对这个数据结构进行修改,那么就会造成并发安全问题。 slave样例伪代码见下代码注释中。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 // 用于保存异步任务执行结果的整型状态码。 // 正数为任务成功,负数为任务失败,0代表尚未完成,可被判成超时。 class TaskResultCode { public: TaskResultCode() : ival_(0) {} int value() const { return ival_; } bool unready() const { return ival_ == 0; } bool succeeded() const { return ival_ > 0; } bool failed() const { return ival_ < 0; } template <int i = 1> void set_succeeded() { static_assert(i > 0, "success code should be positive"); ival_ = i; } template <int i = -1> void set_failed() { static_assert(i < 0, "failure code should be negative"); ival_ = i; } private: std::atomic<int> ival_; }; // 异步任务同步工具。主线程为master,任务线程为slave。 // 主线程发送异步任务时,打包一个SyncKit用作同步,同时还可以传入一个共享数据结构, // 用于让slave把任务执行结果数据保存在此。 class SyncKit { public: SyncKit() = default; // 主线程master检查异步任务执行结果,如果尚未完成则标记已经超时。 const TaskResultCode& master_check_ret() { if (task_done_) { return ret_; } is_timeout_ = true; if (slave_checking_timeout_) { while (!task_done_) {} // waiting return ret_; } return ret_; } // 主线程检master查任务结果,但不标记超时。 const TaskResultCode& master_peek_ret() { return ret_; } /** * slave code example: * * if slave_test_timeout(): * return * if invalid parameters or conditions: * slave_set_failed_unsafe<some failure code> * * slave starts to run a task。 e.g. do some RPC. * * // 异步任务已经完成,准备将任务结果写回到与master共享的数据结构中。 * // 写之前先判断master是否已经标记超时。 * if slave_check_whether_timeout(): * return * * // 尚未超时,master还在等待。此后的代码逻辑必须保证slave一定会调用 * // slave_set_failed<failure code>()或slave_set_succeeded<success code>() * // 并最终对 task_done_ 赋值为true,否则可能阻塞主线程。 * * if task failed: * slave writes failed result to shared data or does nothing. * slave_set_failed<some failure code>() * else: * slave writes succeeded result to shared data. * slave_set_succeeded<some success code>() * */ // 任务线程slave检查主线程master是否已经标记超时。如果已经标记为超时, // 则slave不能再对与master共享的数据结构做任何修改,因为此时master可能正在读取这些数据。 // 任务线程slave在修改与master共享的数据结构之前,必须调用此方法确保尚未超时。 bool slave_check_whether_timeout() { slave_checking_timeout_ = true; if (is_timeout_) { task_done_ = true; return true; } else { return false; } } bool slave_test_timeout() const { return is_timeout_; } template <int failure_code = -1> void slave_set_failed() { ret_.set_failed<failure_code>(); task_done_ = true; } template <int success_code = 1> void slave_set_succeeded() { ret_.set_succeeded<success_code>(); task_done_ = true; } // use default code 1 for succeeded and -1 for failed. void slave_set_ret(bool succeeded) { if (succeeded) { slave_set_succeeded(); } else { slave_set_failed(); } } // unsafe, could call this without calling slave_check_whether_timeout first. // 但是可能导致master观测到的TaskResultCode变化一次,从0变为负数。 template <int failure_code = -1> void slave_set_failed_unsafe() { if (!is_timeout_) { ret_.set_failed<failure_code>(); } task_done_ = true; } private: std::atomic<bool> is_timeout_{false}; std::atomic<bool> slave_checking_timeout_{false}; std::atomic<bool> task_done_{false}; TaskResultCode ret_; }; // 以下是一些配套工具代码 typedef std::shared_ptr<SyncKit> SyncKitPtr; inline SyncKitPtr new_sk() { return std::make_shared<SyncKit>(); } struct SyncKitGuard { SyncKitPtr sk; std::string name; SyncKitGuard() : sk(new_sk()) {} SyncKitGuard(std::string n) : sk(new_sk()), name(std::move(n)) {} SyncKitGuard(std::string n, SyncKitPtr orther_sk) : sk(orther_sk), name( std::move(n)) { assert(sk != nullptr); } SyncKitGuard(SyncKitGuard&& r) : sk(std::move(r.sk)), name(std::move(r.name)) { r.sk = nullptr; } SyncKitGuard(const SyncKitGuard& r) = delete; ~SyncKitGuard() { if (sk) sk->master_check_ret(); } }; struct SyncKitGuardList : public std::list<SyncKitGuard> { SyncKitGuard& new_back(std::string name = "") { push_back(SyncKitGuard(std::move(name))); return back(); } }; struct SyncKitGuardMap : public std::map<std::string, SyncKitGuard> {}; 留一个思考题,SyncKit类中的这些原子bool类型,最低需要使用什么MemoryOrder呢? sec_cst? release-acquire? relaxed?

2022/4/18
articleCard.readMore

什么是好的本地缓存 | What Is a Good Local Cache

缓存技术,Cache,特别是LocalCache,是软件开发中非常常用的组件,也是提高性能的最简单的方式。 Cache一般有SideCar和Wrapper两种使用模式。 Cache的使用模式 SideCar模式:需要应用程序自己主动访问数据源服务并读写缓存。 这种模式可以是LocalCache,与应用程序集成部署,但是由于Cache功能与业务逻辑无耦合,非常独立,所以出现了很多优秀的独立出来的非常通用的第三方独立缓存服务, 比如Redis,Memcached等。 独立缓存服务一般与应用程序分开,独立部署,需要通过网络进行交互,数据量较大或对性能要求较高时,网络带宽消耗、延时、数据序列化反序列化的CPU消耗不能忽视。 1 2 3 4 Cache ↑ | App --> DataSource Wrapper模式:应用程序只需要访问缓存,缓存具有代理能力可以帮助应用程序去访问数据源服务。 这种模式需要对Cache做特殊化定制,一般是LocalCache,与应用程序集成部署,只需在内存中进行交互,性能较高。如果Cache设计的好,可以极大简化业务代码, 可以提高组件可复用性。 1 App --> Cache --> DataSource 那么怎么才算是一个好的LocalCache呢?从业务应用上要灵活好用,满足多种多样的业务需求,同时也要具有基本的高性能。 灵活好用 具有单Key读写接口,也具有Batch读写接口。 可以动态修改CacheSize,一般指缓存的数据条目数。 可配置缓存策略,例如:LRU,LRU-k(lazy list adjustment),FIFO等。 可以设置一个数据的默认过期时间,也可以动态设置每一个Key的过期时间,特别的,在插入或读取时都可以设置和判断过期。 可以配置读取数据源服务的CallbackOnFetching方法,以便应用Wrapper模式,当Key不存在或过期时,用来获取新数据。 过期数据不自动删除,可配置一个CallbackOnExpiration方法来注入对过期数据的处理方式。比如可以删除数据,或把过期数据发送到某个消息队列、写入硬盘等, 或不对过期数据做任何处理,只等缓存用满以后自动逐出。 支持多种QueryFlags: Uncacheable: 本次Query不读写Cache,只从数据源服务获取数据并返回,不写入Cache,相当于没有Cache,只对Query做转发。 Refresh: 本次Query不读取Cache数据,重新从数据源服务获取最新数据,并写入Cache,相当于刷新缓存。 Peek: 本次Query只读取Cache数据,即使Miss或Expired也不从数据源服务获取最新数据,可用于窥测Cache状态,或保护后端数据源服务。 Stale: 本次Query读取Cache数据时,可以接受过期的数据。 StaleFailover: 本次Query如果从远程数据源服务读取数据失败,则可以接受Cache中的过期的数据。 Fast: 本次Query只返回Cache里的数据,如果Miss或Expired,则发送Reload任务刷新缓存,不阻塞当前Query。 Preload: 本次Query如果在Cache中读取到合法数据,但是数据快过期了(比如已过了过期时间的80%,阈值可配置),则发送一个Reload任务来刷新缓存。 ReturnExpired: 本次Query如果在Cache中读到了过期的数据,则把过期的数据也一起返回,供用户自主决定处理方式。 内置线程池可执行Reload任务,从数据源服务获取数据并填充Cache。 可配置内置WatchDog线程,检查快要过期的数据并发送Reload任务,或清理过期数据等。 可迭代操作Cache中的每一条数据,例如把Cache中的数据读出写入到其他设备。 高性能 并发性好。例如采用分桶机制、采用TBB的高性能并发容器库等。 内存使用效率高。

2022/3/31
articleCard.readMore

在线服务参数管理 | Parameters Management for Online Service

互联网公司业务发展快,策略复杂,软件迭代变更非常频繁。因此有必要有一套良好的参数管理思想控制在线服务的表现,以支持业务的频繁变更和AB实验等。 参数分类 默认配置参数:分为远程配置和本地配置。远程配置可动态修改、动态加载,而不需要升级或重启服务,使用方便, 但是有稳定性和可靠性风险,比如由于网络中断、提供远程配置的载体服务异常等原因而导致取不到配置信息。 因此,还需要一套本地配置,随服务一起发布,理论上配置信息应该与远程配置是一模一样的, 但由于本地配置发布频率低,可能会比远程配置版本低,不过问题不大,本地配置主要是对远程配置不可用时做兜底的, 生效时机较少。 实验参数:一般指AB实验的策略配置参数。 降级参数:服务故障时,临时注入的策略参数,用于降低策略复杂度、降低服务消耗、提高服务可用性,可能会对实验指标、效果指标有损。 由于AB实验指标可能对业务发展比较重要,因此降级参数还可以再细分一下优先级,分为优先级高于AB实验参数的高优降级参数和优先级低于AB实验参数的低优降级参数。 发生轻微故障时,使用低优降级参数则不会对实验指标有影响。 优先级 服务初始化时,按照一定优先级合并多种参数用于后续处理。综上描述,按优先级从高到低,这一套在线服务参数的排列顺序是: 1 高优降级参数 -> 实验参数 -> 低优降级参数 -> 默认配置参数(远程配置参数 或 本地配置参数)

2022/3/17
articleCard.readMore

C++轻量级输出库MyOStream:可打印输出所有成员可迭代的容器

懒汉的烦恼 使用C++编程时对数据打印输出比较麻烦,需要自行用for循环将vector, list, map等容器的成员一一打印输出。 相比之下Python, Golang等语言就可以直接对所有数据类型打印输出,这对于debug是很友好的特性。 因此,我开发了一个简单的C++库,几乎能够对所有容器直接打印输出,说几乎是因为我们只能对成员可访问可迭代的容器支持这个特性。 支持的类型如下,以及他们的组合类型: std::pair, std::tuple, std::array, std::deque, std::forward_list, std::initializer_list, std::list, std::vector, std::set, std::multiset, std::unordered_set, std::unordered_multiset, std::map, std::multimap, std::unordered_map, std::unordered_multimap. ACMer助手 特别的,我们在打ACM比赛或做类似的OJ题目的过程中,需要debug的时候,我们不仅想要打印容器里的值, 而且还想要同时打印出变量名,以便我们在解题时,如果定义了多个容器变量,我们可以知道哪个变量的值使哪一个值, 因此我设计了watch功能。 宏定义watch可以用一下两种方式: 一,用std::cout输出MYOSTREAM_WATCH_TO_STRING生成的字符串: 1 2 #include <myostream.h> #define watch(...) std::cout << MYOSTREAM_WATCH_TO_STRING(std::string, " = ", "\n", "\n\n", __VA_ARGS__) 二,直接使用MYOSTREAM_WATCH,坏处是这里定义了一个变量mycout,这段代码不能在头文件中被多个源文件include: 1 2 3 #include <myostream.h> myostream::ostream mycout(std::cout.rdbuf()); #define watch(...) MYOSTREAM_WATCH(mycout, " = ", "\n", "\n\n", __VA_ARGS__) 例如,如下debug代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <vector> #ifndef ONLINE_JUDGE #include <myostream.h> myostream::ostream mycout(std::cout.rdbuf()); #define watch(...) MYOSTREAM_WATCH(mycout, " = ", "\n", "\n\n", __VA_ARGS__) #else #define watch(...) #endif int main() { std::vector<int> v{1, 2, 3}; watch(v); std::tuple<std::vector<int>, std::map<int, std::set<std::string>>, std::pair<std::string, double>> complex({1, 2, 3}, {{1, {"a", "aa"}}, {2, {"b", "bb"}}}, {"Aya", 88.5}); watch(complex); return 0; } 在本地编译运行,可以输出: 1 2 3 v = [1, 2, 3] complex = <[1, 2, 3], {1: {a, aa}, 2: {b, bb}}, (Aya, 88.5)> 而在线提交,或定义了宏ONLINE_JUDGE后,则什么都不输出。 附录 lib MyOStream on github

2022/3/17
articleCard.readMore

字节跳动发展史 | History of Bytedance

2012年3月9日,字节跳动Bytedance成立于北京海淀锦秋家园一个四居室内。天使轮融资,500万元,顺为资本、晨兴资本投资。 2012年5月,内涵段子发布。 2012年7月,A轮融资,500万美元,SIG海纳亚洲独家投资。 2012年8月,今日头条1.0版本上线。 2012年10月,累计用户数超过1000万。 2012年底,1500万激活用户数,近160万DAU。今日头条DAU超过100万? 2013年3月,今日头条日活跃用户突破100万。 2013年5月下旬,B轮融资,5000万美元,俄罗斯投资集团DST独家投资。公司估值千万+美元。 2013年5月,第一次搬家,搬入盈都大厦10层。 2013年8月,今日头条用户数突破5000万。开始尝试商业化。 2013年11月,今日头条和内涵段子激活用户数超过6000万,DAU接近600万,公司人数接近一百人。 2013年12月,累计用户数超过9500万。 2014年4月,今日头条MAU3000万。 2014年6月,今日头条激活用户数1.2亿,MAU4000万。C轮融资,1亿美元,红杉资本、新浪微博基金领投。公司估值5亿美元。 2014年9月,今日头条日活跃用户突破1000万。 2014年12月,累计激活用户超过2亿。 2014年底,公司人数约500人? 2015年1月,今日头条累计用户2.2亿,DAU2000万。 2015年4月,累计激活用户超过2.5亿。 2015年7月,总DAU超过5000万(WAP端+移动端),累计激活用户超过2.7亿。 2015年9月,累计激活用户数超过3亿,DAU超过3000万。 2015年9月8日,今日头条推出头条号“千人万元”计划,同时推出“新媒体孵化器计划”。 2015年12月,举办第一次ByteDance面对面。 2015年底,今日头条DAU3300万,公司人数大约1200人。 2016年2月,今日头条开辟“头条寻人”平台。 2016年2月28日,从盈都搬到了中航广场。 2016年6月,累计激活用户数超过5亿,DAU超过4800万。 2016年8月,今日头条战略投资国内知名图片库东方IC。 2016年8月底,累计激活用户数超过5.5亿,DAU超过6000万。 2016年9月,抖音上线。 2016年9月20日,张一鸣宣布将在未来一年拿出10亿人民币扶持短视频创作者。 2016年10月15日,今日头条领投印度版今日头条Dailyhunt D轮融资。 2016年10月,今日头条激活用户数超过6亿,MAU超过1.4亿,DAU超过6600万,单用户日均使用时长超过76分钟。 2016年12月,D轮融资,10亿美元,建银国际、红杉资本投资。公司估值110亿美元。 2016年底,今日头条累计激活用户数超过7亿,DAU超过7800万。公司人数4千? 2016年营收约60亿元。 2017年2月,5000万美元全资收购美国移动短视频创作者社区Flipagram。 2017年5月,TikTok上线。 2017年9月,E轮融资,2亿美元,老虎基金、美国泛大西洋(600558)投资集团投资。公司估值220亿美元。 2017年11月,5000万美元投资猎豹移动旗下Live.me,8660万美元收购猎豹移动旗下News Republic,10亿美元收购Musical.ly,系彼时最大收购。 Musical.ly MAU6000万,美国MAU2000万,49%的用户都在美国。 2017年11月,公司内部开始全面使用飞书(当时名为Lark)。(飞书2014年底内部成立,2019年起对外开放) 2017年底,公司人数1.3万? 2017年营收约160亿元。 2018年1月,TikTok MAU5500万。 2018年2月,3亿美元收购Faceu激萌。 2018年2月,发布“字节范”(5条)和“领导力原则”。 2018年4月,抖音DAU破亿。 2018年4月10日下午,内涵段子被关停。损失2000万DAU,50亿年收入。 2018年4月,公司品牌名从"今日头条"改为"字节跳动"。 2018年10月,Pre-IPO轮融资,40亿美元,春华资本、软银中国、云锋基金投资。公司估值750亿美元。 2018年12月,TikTok MAU 2.7亿。 2018年底,公司人数3.5万? 2018年营收约500亿元。 2019年1月,发布商业化品牌巨量引擎。 2019年1月,3亿人民币收购锤子科技部分专利使用权,探索教育领域相关业务。 2019年5月,视频编辑工具剪映上线。 2019年7月,成立教育部门ZERO,陈林为负责人。朱文佳接替陈林作为今日头条CEO。 2019年11月,免费阅读产品番茄小说正式上线。 2019年12月,TikTok MAU 5亿。 2019年12月,公司发布使命:激发创造,丰富生活。 2019年底,公司人数6万? 2019年营收约1400亿元。 2020年1月,抖音宣布DAU超过4亿。 2020年3月30日,战略融资,Tiger Global Management(老虎环球基金)投资,未披露金额。公司估值达1000亿美元。 2020年6月,推出企业技术服务平台“火山引擎”。抖音电商正式成立。 2020年6月,员工数7万? 2020年7月,TikTok MAU 6.9亿。 2020年8月,抖音DAU超6亿。 2020年9月,抖音宣布DAU突破6亿(抖音火山合并)。 2020年10月,战略融资,20亿美元,红杉资本、KKR等投资。公司估值1800亿美元。 2020年10月底,教育部门ZERO启用全新品牌"大力教育",陈林出任CEO,教育部门已超过一万人,决心三年不盈利,专心做产品。 2020年11月10日,抖音电商发出第10亿个包裹。 2020年底,全球所有应用程序的月活用户数达19亿。员工数超过十万人,其中正式员工近9万人。 2020年营收约2400亿元。营收343亿美元,毛利润增长93%至190亿美元,经营亏损21亿美元。 2021年1月,抖音生活服务正式启动。 2021年2月,国际电商启航,TikTok Shop上线。 2021年3月,40美元收购沐瞳科技(100亿人民币现金和价值150亿人民币的股权)。 2021年5月20日,张一鸣宣布卸任CEO,梁汝波继任,年底完成交接。 2021年6月,字节跳动全线产品MAU超19亿。 2021年8月,收购VR创业公司Pico,50亿元?15亿美元(97亿人民币)?正式组建字节XR业务线,并积极拓展全球市场。 2021年9月,TikTok MAU 10亿,DAU 6亿。 2021年11月,梁汝波宣布组织调整,成立6个业务板块:抖音(包括头条、西瓜、搜索、百科等国内信息和服务业务)、大力教育、飞书、 火山引擎(企业级技术服务云平台)、朝夕光年(游戏业务)、TikTok。 2021年营收580亿美元,约3700亿人民币,同比增长70%。 2022年4月,汽水音乐上线。 2022年6月,更新字节范和领导力原则。(6条,4个板块) 2023年2月,抖音寻人7年帮助20000个家庭团圆。 2023年3月,豆包App正式上线。 2023年5月,红果短剧正式上线。 2024年5月,豆包大模型正式发布。 2024年7月,即梦AI正式上线。 2024年8月,TikTok Shop首个200万美元直播间诞生。 2024年10月,豆包发布首款AI智能体耳机Ola Friend。

2022/3/4
articleCard.readMore

About

关于我 李双全,男,90后,天蝎座,河北唐山人,程序员,现居北京。 工作经历 2017-2022,北京,字节跳动,后端开发工程师。评论排序系统架构和策略,推荐系统架构和策略。 教育经历 2010-2017,南京,东南大学,自动化学院,获工学学士和工学硕士学位。 业余爱好 徒步⛰,骑行🚴‍♂️,跑步🏃🏻,健身💪,篮球🏀,羽毛球🏸,乒乓球🏓; 读书📖,旅行🌴,写字✍️,诗歌📓,民谣🎧,逛书店,看展🖼,看音乐剧等等。 联系我 邮箱:lishq991 艾特 gmail 点 com 关于本站 Peacalm寓意: peaceful & calm Jane peacalm.github.io或我的个人域名lishuangquan.cn都可以访问本站

2022/3/2
articleCard.readMore