写点什么

C++ 动态新闻推送 第 36 期

作者:很水
  • 2021 年 11 月 12 日
  • 本文字数:14060 字

    阅读完需:约 46 分钟


reddit/hackernews/lobsters/meetingcpp摘抄一些 c++动态


每周更新


周刊项目地址在线地址知乎专栏 |腾讯云+社区


欢迎投稿,推荐或自荐文章/软件/资源等,请提交 issue


cppcon2021 开完了,陆续把视频发出来,有几个更几个


meetingcpp 这周也开始了,也是一周,所以这个十一月视频多的要死看不过来


想直接看的直接往下拉到视频环节



资讯

编译器信息最新动态推荐关注 hellogcc 公众号

OSDT Weekly 2021-11-03 第122期

文章


还是 concept,复读几遍就记住了,这里在啰嗦一下


#include <concepts>#include <iostream> template<std::integral T>void overloaded(T a){   std::cout << "Integral overload called with " << a << std::endl;} template<std::integral T> requires (sizeof(T) == 2)void overloaded(T a){  std::cout << "Short overload called with " << a << std::endl;} int main(){  int a{10};  short b{20};  overloaded(a);  overloaded(b);}
复制代码


你学会了吗~



又一个 range 教程,复读几遍读者就记住了,这里在啰嗦一下


替代 stl


std::vector<int> dt = {1, 4, 2, 3};std::ranges::sort(dt);
复制代码


映射


struct Account {    std::string owner;    double value();    double base();};std::vector<Account> acc = get_accounts();// memberstd::ranges::sort(acc,{},&Account::owner);// member functionstd::ranges::sort(acc,{},&Account::value);// lambdastd::ranges::sort(acc,{},[](const auto& a) {     return a.value()+a.base(); });
复制代码


view


namespace rv = std::ranges::views;std::vector<int> dt = {1, 2, 3, 4, 5, 6, 7};for (int v : rv::reverse(rv::take(rv::reverse(dt),3))) {    std::cout << v << ", ";}std::cout << "\n";
复制代码


你学会了吗~



python 有 GIL 大锁,最近社区突然有个牛人实现了一个去掉 GIL 的 python,性能有提升,单核 9%


这篇文章介绍一些相关的信息


  • nogil 版本从 3.9 拉出来的,现在已经 3.11 了,一时半会用不上,代码可能需要很久

  • 3.11 也有很多性能优化的提升,nogil 版本比 3.9 快 ,但没有 3.11 快 (单核) 更细致的比较数据暂时没有

  • 使用 mimalloc 替换内部 pymalloc mimalloc 在大量小对象场景上有很好的提升效果,且 mimalloc 本身的设计,竞争轻量

  • 使用biased reference counting 降低引用计数的开销 论文在这

  • 对象和自身的线程绑定

  • 本地线程访问对象无需原子访问

  • 其他线程访问还有有引用计数的增减的

  • 一些对象就不计数了; 比如 None, True, False, 内部字符串,小数字等等, PyTypeObjects for built-in types

  • 一些对象就推迟计算引用计数了,module 代码块这些,可能和程序的生命周期相同


这里面 mimalloc 带来的提升很有效果,且 mimalloc 也有合作帮助优化 cpython,这个 mimalloc 值得研究研究


另外,微软又出了个 snmalloc,也有一些 mimalloc 的设计,也值得研究研究



手把手教你用 goto


多层 break 场景


    for (int i = 0; i < 3; i++) {        for (int j = 0; j < 3; j++) {            for (int k = 0; k < 3; k++) {                printf("%d, %d, %d\n", i, j, k);
goto continue_i; // Now continuing the i loop!! } }continue_i: ; }
复制代码


这种场景下,break continue 都无法替代 (或者别这么写代码)


错误处理场景


   for(...) {        for (...) {            while (...) {                do {                    if (some_error_condition)                        goto bail;
} while(...); } } }
bail:
复制代码


还有一种, 逐层清理 (c++有 RAII 处理这个)


    if (init_system_1() == -1)        goto shutdown;
if (init_system_2() == -1) goto shutdown_1;
if (init_system_3() == -1) goto shutdown_2;
if (init_system_4() == -1) goto shutdown_3;
do_main_thing(); // Run our program
shutdown_system4();
shutdown_3: shutdown_system3();
shutdown_2: shutdown_system2();
shutdown_1: shutdown_system1();
shutdown: print("All subsystems shut down.\n");
复制代码


retry 循环


retry:    byte_count = read(0, buf, sizeof(buf) - 1);  // Unix read() syscall
if (byte_count == -1) { // An error occurred... if (errno == EINTR) { // But it was just interrupted printf("Restarting...\n"); goto retry; }
复制代码


while 也可以


坑爹场景,goto 和局部变量处理,局部变量生命周期要在 goto 的 lable 后可见



template<class, std::size_t> concept Any = true;
constexpr auto last1 = [](auto... args) { return [&]<std::size_t... Ns>(std::index_sequence<Ns...>) { return [](Any<Ns> auto..., auto last) { return last; }(args...); } (std::make_index_sequence<sizeof...(args) - 1>{});};
auto last2 = [](auto... args) { return (args, ...);};
static_assert(1 == last1(1));static_assert(2 == last1(1, 2));static_assert(3 == last1(1, 2, 3));
static_assert(1 == last2(1));static_assert(2 == last2(1, 2));static_assert(3 == last2(1, 2, 3))
复制代码


这个 last2 有点意思,不是新的东西 c++17 也能编的过


godbolt



吐槽 coroutine 难用,比如不是零开销抽象,api 难用太低层之类的



一种代码 api 风格转换,套上观察者模式,代码在这里,可以看看



一个测试,malloc 并没有返回报错而是直接崩溃了?


#include <stdio.h>#include <stdlib.h>
int main() { size_t large = 1099511627776; char *buffer = (char *)malloc(large); if (buffer == NULL) { printf("error!\n"); return EXIT_FAILURE; } printf("Memory allocated\n"); for (size_t i = 0; i < large; i += 4096) { buffer[i] = 0; } free(buffer); return EXIT_SUCCESS;}
复制代码


分配的内存是虚拟内存,不是物理内存,直接访问,访问出 segfault 了,真奇怪



视频


auto 可以用 concept 来修饰,从而实现限制 auto。想要让 auto 在指定的 concept 下面 auto



讲设计原理的。。。

cppcon 结束了,jetbrains 作为赞助商提前放出来了一些视频 这里简单过一下


这个之前说过,就是 c++引入模式匹配,引入 is as 关键字


void f(auto const& x) {  inspect (x) {    i as int           => std::cout << "int " << i;    [_,y] is [0,even]  => std::cout << "point on y-axis and even y " << y;    [a,b] is [int,int] => std::cout << "2-int tuple " << a << " " << b;    s as std::string   => std::cout << "string \"" + s + "\"";    is _               => std::cout << "((no matching value))";  }}
int main() { f(42); f(std::pair{0, 2}); f(std::tuple{1, 2}); f("str"); struct {} foo; f(foo);}
复制代码



和 std::find 没关系啊,这个就是讨论语义的,条件,限制之类的,Sean Paren 大哥讲故事,还讲了几个冷笑话



没什么意思,讲语义的。给我听困了



单片机越来越廉价普及,应该大力发挥 c++在其中的作用。然后说了一大堆概念。怎么都这么抽象

项目


之前也说过这个思路,fixed_string + UDL


#include <algorithm>#include <any>#include <experimental/iterator>#include <iostream>#include <iterator>#include <string_view>#include <tuple>#include <type_traits>#include <utility>#include <vector>
template <auto Size>struct fixed_string { char data[Size + 1]{}; static constexpr auto size = Size;
constexpr explicit(false) fixed_string(char const* str) { std::copy_n(str, Size + 1, data); } constexpr explicit(false) operator std::string_view() const { return {data, Size}; }};template <auto Size>fixed_string(char const (&)[Size]) -> fixed_string<Size - 1>;
using std::literals::string_view_literals::operator""sv;
static_assert(""sv == fixed_string(""));static_assert("name"sv == fixed_string("name"));
template <fixed_string Name, class TValue>struct arg { static constexpr auto name = Name; TValue value{}; template <class T> constexpr auto operator=(const T& t) { return arg<Name, T>{.value = t}; }};
namespace detail {template <class TDefault, fixed_string, template <fixed_string, class> class>auto map_lookup(...) -> TDefault;template <class, fixed_string TKey, template <fixed_string, class> class TArg, class TValue>auto map_lookup(TArg<TKey, TValue>*) -> TArg<TKey, TValue>;
template <class TDefault, class, template <class, class> class>auto map_lookup(...) -> TDefault;template <class, class TKey, template <class, class> class TArg, class TValue>auto map_lookup(TArg<TKey, TValue>*) -> TArg<TKey, TValue>;} // namespace detail
template <class T, fixed_string TKey, class TDefault, template <fixed_string, class> class TArg>using map_lookup = decltype(detail::map_lookup<TDefault, TKey, TArg>( static_cast<T*>(nullptr)));
template <class... Ts>struct inherit : Ts... {};
static_assert(std::is_same_v< void, map_lookup<inherit<arg<"price", double>, arg<"size", int>>, "unknown", void, arg>>);static_assert( std::is_same_v<arg<"price", double>, map_lookup<inherit<arg<"price", double>, arg<"size", int>>, "price", void, arg>>);static_assert( std::is_same_v<arg<"size", int>, map_lookup<inherit<arg<"price", double>, arg<"size", int>>, "size", void, arg>>);
struct any : std::any { any() = default; template <class T> explicit(false) any(const T& a) : std::any{a}, print{[](std::ostream& os, const std::any& a) -> std::ostream& { if constexpr (requires { os << std::any_cast<T>(a); }) { os << std::any_cast<T>(a); } else if constexpr (requires { std::begin(std::any_cast<T>(a)); std::end(std::any_cast<T>(a)); }) { auto obj = std::any_cast<T>(a); std::copy(std::begin(obj), std::end(obj), std::experimental::make_ostream_joiner(os, ',')); } else { os << a.type().name(); } return os; }} {} template <class T> constexpr explicit(false) operator T() const { return std::any_cast<T>(*this); }
friend std::ostream& operator<<(std::ostream& os, const any& a) { return a.print(os, a); }
private: std::ostream& (*print)(std::ostream&, const std::any&){};};
template <fixed_string Name>constexpr auto operator""_t() { return arg<Name, any>{};}
template <class T, class... TArgs>decltype(void(T{std::declval<TArgs>()...}), std::true_type{}) test_is_braces_constructible(int);template <class, class...>std::false_type test_is_braces_constructible(...);template <class T, class... TArgs>using is_braces_constructible = decltype(test_is_braces_constructible<T, TArgs...>(0));
struct any_type { template <class T> constexpr operator T(); // non explicit};
template <class T>constexpr auto to_tuple(T object) noexcept { using type = std::decay_t<T>; if constexpr (is_braces_constructible<type, any_type, any_type, any_type, any_type, any_type, any_type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); } else if constexpr (is_braces_constructible<type, any_type, any_type, any_type, any_type, any_type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4, p5, p6, p7, p8, p9] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4, p5, p6, p7, p8, p9); } else if constexpr (is_braces_constructible< type, any_type, any_type, any_type, any_type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4, p5, p6, p7, p8] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4, p5, p6, p7, p8); } else if constexpr (is_braces_constructible<type, any_type, any_type, any_type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4, p5, p6, p7] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4, p5, p6, p7); } else if constexpr (is_braces_constructible<type, any_type, any_type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4, p5, p6] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4, p5, p6); } else if constexpr (is_braces_constructible<type, any_type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4, p5] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4, p5); } if constexpr (is_braces_constructible<type, any_type, any_type, any_type, any_type>{}) { auto [p1, p2, p3, p4] = std::forward<T>(object); return std::tuple(p1, p2, p3, p4); } else if constexpr (is_braces_constructible<type, any_type, any_type, any_type>{}) { auto [p1, p2, p3] = std::forward<T>(object); return std::tuple(p1, p2, p3); } else if constexpr (is_braces_constructible<type, any_type, any_type>{}) { auto [p1, p2] = std::forward<T>(object); return std::tuple(p1, p2); } else if constexpr (is_braces_constructible<type, any_type>{}) { auto [p1] = std::forward<T>(object); return std::tuple(p1); } else { return std::tuple{}; }}
// static_assert("name"sv == ("name"_t = 42).name);// static_assert(42 == ("name"_t = 42).value);template <class T>[[nodiscard]] constexpr auto type_name() -> std::string_view {#if defined(_MSC_VER) and not defined(__clang__) return {&__FUNCSIG__[120], sizeof(__FUNCSIG__) - 128};#elif defined(__clang_analyzer__) return {&__PRETTY_FUNCTION__[57], sizeof(__PRETTY_FUNCTION__) - 59};#elif defined(__clang__) and (__clang_major__ >= 12) and not defined(__APPLE__) return {&__PRETTY_FUNCTION__[34], sizeof(__PRETTY_FUNCTION__) - 36};#elif defined(__clang__) return {&__PRETTY_FUNCTION__[70], sizeof(__PRETTY_FUNCTION__) - 72};#elif defined(__GNUC__) return {&__PRETTY_FUNCTION__[85], sizeof(__PRETTY_FUNCTION__) - 136};#endif}
namespace nt {template <class B, class... Ts>struct namedtuple : B, private Ts... { static constexpr auto name_v = [] { if constexpr (requires { B::name; }) { return B::name; } else { return type_name<B>(); } }(); static inline std::vector<std::string_view> names;
constexpr explicit(true) namedtuple(Ts... ts) : B{[=] { if constexpr (requires { B{ts.value...}; }) { return B{ts.value...}; } else { return B{}; } }()}, Ts{ts}... { names = {ts.name...}; }
template <class B_, class... Ts_> auto& operator=(const namedtuple<B_, Ts_...>& other) { names = {Ts_::name...}; static_cast<B&>(*this) = static_cast<const B&>(other); return *this; }
template <class T, class TArg = map_lookup<namedtuple, T::name, void, arg>> constexpr const auto& operator[](const T) const requires(not std::is_void_v<TArg>) { return static_cast<const TArg&>(*this).value; }
template <class T, class TArg = map_lookup<namedtuple, T::name, void, arg>> constexpr auto& operator[](const T) requires(not std::is_void_v<TArg>) { return static_cast<TArg&>(*this).value; }
auto& assign(auto&&... ts) { if constexpr ((requires { ts.name; ts.value; } and ...)) { ((static_cast<decltype(ts)&>(*this) = ts), ...); } else { ((static_cast<Ts&>(*this).value = ts), ...); } return *this; }
template <std::size_t N> auto& get() { auto id_type = []<auto... Ns>(std::index_sequence<Ns...>) { return inherit< std::pair<std::integral_constant<std::size_t, Ns>, Ts>...>{}; } (std::make_index_sequence<sizeof...(Ts)>{}); return static_cast< typename decltype(detail::map_lookup< void, std::integral_constant<std::size_t, N>, std::pair>(&id_type))::second_type&>(*this); }
template <std::size_t N> const auto& get() const { auto id_type = []<auto... Ns>(std::index_sequence<Ns...>) { return inherit< std::pair<std::integral_constant<std::size_t, Ns>, Ts>...>{}; } (std::make_index_sequence<sizeof...(Ts)>{}); return static_cast< const typename decltype(detail::map_lookup< void, std::integral_constant<std::size_t, N>, std::pair>(&id_type))::second_type&>(*this); }
friend std::ostream& operator<<(std::ostream& os, const namedtuple& nt) requires(sizeof...(Ts) > 0) { os << std::string_view{name_v} << '{'; [&]<auto... Ns>(std::index_sequence<Ns...>) { ((os << (Ns ? "," : "") << std::string_view{Ts::name} << ':' << static_cast<const map_lookup<namedtuple, Ts::name, void, arg>&>( nt) .value), ...); } (std::make_index_sequence<sizeof...(Ts)>{}); os << '}'; return os; }
friend std::ostream& operator<<( std::ostream& os, const namedtuple& nt) requires(sizeof...(Ts) == 0) { os << std::string_view{name_v} << '{'; auto t = to_tuple(static_cast<const B&>(nt)); [&]<auto... Ns>(std::index_sequence<Ns...>) { ((os << (Ns ? "," : "") << std::string_view{nt.names[Ns]} << ':' << std::get<Ns>(t)), ...); } (std::make_index_sequence<std::tuple_size_v<decltype(t)>>{}); os << '}'; return os; }};template <class... Ts>namedtuple(Ts...) -> namedtuple<void, Ts...>;} // namespace nt
template <class T, class... Ts>constexpr auto namedtuple(Ts... ts) { return nt::namedtuple<T, Ts...>(ts...);}
template <fixed_string Name, class... Ts>constexpr auto namedtuple(Ts... ts) { return nt::namedtuple<arg<Name, std::any>, Ts...>(ts...);}
int main() { // namedtuple in-place const auto nt1 = namedtuple<"s">("price"_t = 42., "size"_t = 100); std::cout << nt1["price"_t] << ',' << nt1["size"_t] << '\n' << nt1 << '\n';
// namedtuple in-place/struct struct s { double price; int size; }; const auto nt2 = namedtuple<s>("price"_t = 42., "size"_t = 100); std::cout << nt2["price"_t] << ',' << nt2["size"_t] << '\n' << nt2.price << ',' << nt2.size << '\n' << nt2 << '\n';
// namedtuple struct nt::namedtuple<s> nt3; nt3 = namedtuple<s>("price"_t = 42., "size"_t = 100); std::cout << nt3.price << ',' << nt3.size << '\n' << nt3 << '\n';}
复制代码



也直接贴代码


// Pretty-printer for `struct` layout and padding bytes// by Vittorio Romeo (@supahvee1234) (https://vittorioromeo.info)
#include <boost/pfr.hpp>#include <iostream>#include <tuple>#include <utility>#include <type_traits>#include <typeinfo>#include <cmath>#include <iomanip>#include <cstring>#include <array>
namespace detail{ // ------------------------------------------------------------------------------ // Round `x` up to the nearest multiple of `mult`. [[nodiscard]] constexpr std::size_t round_up(const std::size_t x, const std::size_t mult) noexcept { return ((x + mult - 1) / mult) * mult; }
// ------------------------------------------------------------------------------ // Recursively print the memory layout of `T` using identation `indent` and // keeping track of the total occupied bytes in `used`. template <typename T> void print_layout_impl(const std::size_t indent, std::size_t& used) { // ------------------------------------------------------------------------------ // Utilities. const char* const t_name = typeid(T).name(); const std::size_t line_length = std::strlen(t_name) + 32;
const auto print_indent = [&](const std::size_t spacing = 1) { for (std::size_t i = 0; i <= indent; ++i) { std::cout << ((i % 2 == 0) ? '|' : ' '); } for (std::size_t i = 0; i < spacing; ++i) { std::cout << ' '; } };
const auto print_line = [&] { print_indent(0); for (std::size_t i = 0; i < line_length; ++i) { std::cout << '-'; } std::cout << '\n'; };
// ------------------------------------------------------------------------------ // Tuple type of all data members of `T`, in order. Used to reflect on `T`. using tuple_type = decltype(boost::pfr::structure_to_tuple(std::declval<T>()));
// ------------------------------------------------------------------------------ // We use a pointer to `std::tuple` to support non-default-constructible types. // We need this inner lambda so that we can use `Ts...` as a pack. [&]<typename... Ts>(std::tuple<Ts...>*) { // ------------------------------------------------------------------------------ // All alignments of the data members, with the alignment of `T` at the end. constexpr std::array alignments{alignof(Ts)..., alignof(T)};
// ------------------------------------------------------------------------------ // Information printed in the header. constexpr std::size_t sum_of_member_sizes = (sizeof(Ts) + ...); constexpr std::size_t total_padding_bytes = (sizeof(T) - sum_of_member_sizes);
// ------------------------------------------------------------------------------ // Print the header. print_line(); print_indent(); std::cout << t_name << " {size: " << sizeof(T) << " (" << sum_of_member_sizes << "# " << total_padding_bytes << "p), align: " << alignof(T) << "}\n";
print_line();
std::size_t type_idx = 0;
// ----------------------------------------------------------------------------- // Print padding in relation to a given alignment const auto print_padding = [&](const std::size_t alignment) { const std::size_t padding = round_up(used, alignment) - used; for (int i = 0; i < padding; ++i) { std::cout << 'p'; } used += padding; };
// ----------------------------------------------------------------------------- // Non-recursively print a fundamental/pointer/reference type const auto print_fundamental = [&]<typename X> { print_indent(); std::cout << std::setw(2) << used << ": [";
print_padding(alignments[type_idx]);
for (std::size_t i = 0; i < sizeof(X); ++i) { std::cout << '#'; } used += sizeof(X); print_padding(alignments[type_idx + 1]);
std::cout << "] " << typeid(X).name() << '\n'; };
// ----------------------------------------------------------------------------- // Recursively print all data members ([&] { if constexpr(std::is_fundamental_v<Ts> || std::is_pointer_v<Ts> || std::is_reference_v<Ts>) { print_fundamental.template operator()<Ts>(); } else { print_layout_impl<Ts>(indent + 2, used); }
++type_idx; }(), ...); }(static_cast<tuple_type*>(nullptr));
print_line(); }}
template <typename T>void print_layout(){ std::size_t used = 0; detail::print_layout_impl<T>(0 /* indent */, used /* used */);}
int main(){ struct foo1 { char *p; /* 8 bytes */ char c; /* 1 byte char pad[7]; 7 bytes */ long x; /* 8 bytes */ };
print_layout<foo1>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct foo2 { char c; /* 1 byte char pad[7]; 7 bytes */ char* p; /* 8 bytes */ long x; /* 8 bytes */ };
print_layout<foo2>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct foo3 { char* p; /* 8 bytes */ char c; /* 1 byte char pad[7]; 7 bytes */ };
print_layout<foo3>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct foo4 { short s; /* 2 bytes */ char c; /* 1 byte char pad[1]; 1 byte */ };
print_layout<foo4>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct foo5 { char c; /* 1 byte char pad1[7]; 7 bytes */
struct foo5_inner { char* p; /* 8 bytes */ short x; /* 2 bytes char pad2[6]; 6 bytes */ } inner; };
print_layout<foo5::foo5_inner>(); std::cout << '\n';
print_layout<foo5>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct foo10 { char c; /* 1 byte char pad1[7]; 7 bytes */ foo10* p; /* 8 bytes */ short x; /* 2 bytes char pad2[6]; 6 bytes */ };
print_layout<foo10>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct foo11 { foo11* p; /* 8 bytes */ short x; /* 2 bytes */ char c; /* 1 byte char pad[5]; 5 bytes */ };
print_layout<foo11>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct test0 { int i; char c; float f; };
print_layout<test0>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct test1 { int i; double d; char c; float f; };
print_layout<test1>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct test2 { void* p0; test0 t0; void* p1; test1 t1; void* p2; }; print_layout<test2>(); std::cout << '\n';
// ------------------------------------------------------------------------------
struct test2_flat { void* p0; int i0; char c0; float f0; void* p1; int i1; double d0; char c1; float f1; void* p2; }; print_layout<test2_flat>(); std::cout << '\n';}
复制代码




看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持!


本文永久链接

发布于: 3 小时前阅读数: 8
用户头像

很水

关注

还未添加个人签名 2020.10.21 加入

还未添加个人简介

评论

发布
暂无评论
C++ 动态新闻推送 第36期