C++20概述

C++20

概述

C++20 包含以下新的语言特性:

C++20 包含以下新的库特性:

C++20 语言特性

协程

注意 :尽管这些示例从基础层面展示了如何使用协程,但代码编译时背后还有大量复杂的逻辑。这些示例并非旨在全面涵盖 C++20 协程的所有内容。由于标准库尚未提供 generatortask 类,笔者使用了 cppcoro 库来编译这些示例。

协程 是一种特殊的函数,其执行过程可被挂起和恢复。要定义一个协程,函数体中必须包含 co_returnco_awaitco_yield 关键字。C++20 的协程是无栈的;除非编译器将其优化掉,否则它们的状态会分配在堆上。

协程的一个示例是生成器(generator) 函数,它会在每次调用时生成(产出)一个值:

c++ 复制代码
generator<int> range(int start, int end) {
  while (start < end) {
    co_yield start;
    start++;
  }

  // 函数末尾会隐式执行 co_return:
  // co_return;
}

for (int n : range(0, 10)) {
  std::cout << n << std::endl;
}

上述 range 生成器函数会从 start 开始生成值,直到 end(不包含)为止,每次迭代步骤都会产出 start 中存储的当前值。生成器会在 range 的每次调用(本例中是 for 循环的每次迭代)之间维持其状态。co_yield 接收给定的表达式,产出(返回)其值,并在此处挂起协程。恢复执行时,会从 co_yield 之后的代码继续。

协程的另一个示例是任务(task),它是一种异步计算,会在等待该任务时执行:

c++ 复制代码
task<void> echo(socket s) {
  for (;;) {
    auto data = co_await s.async_read();
    co_await async_write(s, data);
  }

  // 函数末尾会隐式执行 co_return:
  // co_return;
}

本例中引入了 co_await 关键字。该关键字接收一个表达式,如果等待的对象(此处为读或写操作)尚未就绪,则挂起执行;否则继续执行。(注意,底层实现中 co_yield 会使用 co_await。)

使用任务惰性求值一个值:

c++ 复制代码
task<int> calculate_meaning_of_life() {
  co_return 42;
}

auto meaning_of_life = calculate_meaning_of_life();
// ...
co_await meaning_of_life; // 结果为 42

概念

概念(Concepts) 是具名的编译期谓词,用于约束类型。其语法形式如下:

复制代码
template <模板参数列表>
concept 概念名 = 约束表达式;

其中 约束表达式 会求值为一个 constexpr 布尔值。约束 应描述类型的语义要求,例如某类型是否为数值类型、是否可哈希等。如果给定类型不满足其绑定的概念(即 约束表达式 返回 false),编译器会抛出错误。由于约束在编译期求值,它们能提供更易理解的错误信息,并保障运行时安全。

c++ 复制代码
// `T` 不受任何约束限制
template <typename T>
concept always_satisfied = true;
// 限制 `T` 为整数类型
template <typename T>
concept integral = std::is_integral_v<T>;
// 限制 `T` 同时满足 `integral` 约束且为有符号类型
template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;
// 限制 `T` 同时满足 `integral` 约束且不满足 `signed_integral` 约束
template <typename T>
concept unsigned_integral = integral<T> && !signed_integral<T>;

有多种语法形式可用于强制执行概念约束:

c++ 复制代码
// 函数参数的约束形式:
// `T` 是受约束的类型模板参数
template <my_concept T>
void f(T v);

// `T` 是受约束的类型模板参数
template <typename T>
  requires my_concept<T>
void f(T v);

// `T` 是受约束的类型模板参数
template <typename T>
void f(T v) requires my_concept<T>;

// `v` 是受约束的推导参数
void f(my_concept auto v);

// `v` 是受约束的非类型模板参数
template <my_concept auto v>
void g();

// 自动推导变量的约束形式:
// `foo` 是受约束的自动推导值
my_concept auto foo = ...;

// Lambda 表达式的约束形式:
// `T` 是受约束的类型模板参数
auto f = []<my_concept T> (T v) {
  // ...
};
// `T` 是受约束的类型模板参数
auto f = []<typename T> requires my_concept<T> (T v) {
  // ...
};
// `T` 是受约束的类型模板参数
auto f = []<typename T> (T v) requires my_concept<T> {
  // ...
};
// `v` 是受约束的推导参数
auto f = [](my_concept auto v) {
  // ...
};
// `v` 是受约束的非类型模板参数
auto g = []<my_concept auto v> () {
  // ...
};

requires 关键字可用于启动 requires 子句或 requires 表达式:

c++ 复制代码
template <typename T>
  requires my_concept<T> // `requires` 子句
void f(T);

template <typename T>
concept callable = requires (T f) { f(); }; // `requires` 表达式

template <typename T>
  requires requires (T x) { x + x; } // 同一行同时出现 `requires` 子句和表达式
T add(T a, T b) {
  return a + b;
}

注意,requires 表达式中的参数列表是可选的。requires 表达式中的每个约束属于以下类型之一:

  • 简单约束 - 断言给定表达式是合法的。
c++ 复制代码
template <typename T>
concept callable = requires (T f) { f(); };
  • 类型约束 - 以 typename 关键字后跟类型名表示,断言给定类型名是合法的。
c++ 复制代码
struct foo {
  int foo;
};

struct bar {
  using value = int;
  value data;
};

struct baz {
  using value = int;
  value data;
};

// 使用 SFINAE,仅当 `T` 是 `baz` 时启用
template <typename T, typename = std::enable_if_t<std::is_same_v<T, baz>>>
struct S {};

template <typename T>
using Ref = T&;

template <typename T>
concept C = requires {
                     // 对类型 `T` 的约束:
  typename T::value; // A) 拥有名为 `value` 的内部成员
  typename S<T>;     // B) 必须存在合法的类模板特化 `S<T>`
  typename Ref<T>;   // C) 必须是合法的别名模板替换
};

template <C T>
void g(T a);

g(foo{}); // 错误:不满足约束 A
g(bar{}); // 错误:不满足约束 B
g(baz{}); // 正确
  • 复合约束 - 大括号中的表达式后跟后置返回类型或类型约束。
c++ 复制代码
template <typename T>
concept C = requires(T x) {
  {*x} -> std::convertible_to<typename T::inner>; // 表达式 `*x` 的类型可转换为 `T::inner`
  {x + 1} -> std::same_as<int>; // 表达式 `x + 1` 满足 `std::same_as<decltype((x + 1))>`
  {x * 1} -> std::convertible_to<T>; // 表达式 `x * 1` 的类型可转换为 `T`
};
  • 嵌套约束 - 以 requires 关键字表示,指定额外的约束(例如对局部参数的约束)。
c++ 复制代码
template <typename T>
concept C = requires(T x) {
  requires std::same_as<sizeof(x), size_t>;
};

另见:概念库

三路比较

C++20 引入了太空船运算符(<=>),作为编写比较函数的新方式,可减少样板代码,并帮助开发者定义更清晰的比较语义。定义三路比较运算符后,编译器会自动生成其他比较运算符函数(即 ==!=< 等)。

C++20 引入了三种排序类型:

  • std::strong_ordering:强排序区分"相等"(完全相同且可互换)的元素。包含 less(更小)、greater(更大)、equivalent(等价)和 equal(相等)四种排序结果。适用场景示例:在列表中查找特定值、整数比较、区分大小写的字符串比较。
  • std::weak_ordering:弱排序区分"等价"(不完全相同,但比较时可互换)的元素。包含 lessgreaterequivalent 三种排序结果。适用场景示例:不区分大小写的字符串比较、排序操作、仅比较类的部分可见成员。
  • std::partial_ordering:部分排序遵循弱排序的原则,但包含"无法比较"的情况。包含 lessgreaterequivalentunordered(无序,无法比较)四种排序结果。适用场景示例:浮点数值比较(例如 NaN)。

默认的三路比较运算符会按成员逐一比较:

c++ 复制代码
struct foo {
  int a;
  bool b;
  char c;

  // 先比较 `a`,再比较 `b`,最后比较 `c`......
  friend auto operator<=>(const foo&) const = default;
};

foo f1{0, false, 'a'}, f2{0, true, 'b'};
f1 < f2; // 结果为 true
f1 == f2; // 结果为 false
f1 >= f2; // 结果为 false

也可自定义比较逻辑:

c++ 复制代码
struct foo {
  int x;
  bool b;
  char c;

  friend std::strong_ordering operator<=>(const foo& other) const {
      return x <=> other.x;
  }
};

foo f1{0, false, 'a'}, f2{0, true, 'b'};
f1 < f2; // 结果为 false
f1 == f2; // 结果为 true
f1 >= f2; // 结果为 true

指定初始化器

C 风格的指定初始化器语法。未在指定初始化器列表中显式列出的成员字段会被默认初始化。

c++ 复制代码
struct A {
  int x;
  int y;
  int z = 123;
};

A a {.x = 1, .z = 2}; // a.x == 1,a.y == 0,a.z == 2

Lambda 表达式的模板语法

在 Lambda 表达式中使用熟悉的模板语法。

c++ 复制代码
auto f = []<typename T>(std::vector<T> v) {
  // ...
};

带初始化器的基于范围的 for 循环

该特性简化了常见的代码模式,有助于缩小变量作用域,并为一个常见的生命周期问题提供了优雅的解决方案。

c++ 复制代码
for (auto v = std::vector{1, 2, 3}; auto& e : v) {
  std::cout << e;
}
// 输出 "123"

[[likely]] 和 [[unlikely]] 属性

向优化器提供提示,表明标记的语句被执行的概率很高/很低。

c++ 复制代码
switch (n) {
case 1:
  // ...
  break;

[[likely]] case 2:  // 认为 n == 2 的概率远高于 n 的其他取值
  // ...            
  break;
}

如果 likely/unlikely 属性出现在 if 语句的右括号之后,表明该分支的子语句(函数体)被执行的概率很高/很低。

c++ 复制代码
int random = get_random_number_between_x_and_y(0, 3);
if (random > 0) [[likely]] {
  // if 语句体
  // ...
}

该属性也可应用于迭代语句的子语句(循环体):

c++ 复制代码
while (unlikely_truthy_condition) [[unlikely]] {
  // while 语句体
  // ...
}

弃用 this 的隐式捕获

在 Lambda 捕获中通过 [=] 隐式捕获 this 现已被弃用;建议通过 [=, this][=, *this] 显式捕获。

c++ 复制代码
struct int_value {
  int n = 0;
  auto getter_fn() {
    // 不良写法:
    // return [=]() { return n; };

    // 良好写法:
    return [=, *this]() { return n; };
  }
};

非类型模板参数中的类类型

类类型现在可用于非类型模板参数。作为模板实参传递的对象类型为 const TT 是对象的类型),且拥有静态存储期。

c++ 复制代码
struct foo {
  foo() = default;
  constexpr foo(int) {}
};

template <foo f = {}>
auto get_foo() {
  return f;
}

get_foo(); // 使用隐式构造函数
get_foo<foo{123}>();

constexpr 虚函数

虚函数现在可以是 constexpr 的,并能在编译期求值。constexpr 虚函数可以覆盖非 constexpr 虚函数,反之亦然。

c++ 复制代码
struct X1 {
  virtual int f() const = 0;
};

struct X2: public X1 {
  constexpr virtual int f() const { return 2; }
};

struct X3: public X2 {
  virtual int f() const { return 3; }
};

struct X4: public X3 {
  constexpr virtual int f() const { return 4; }
};

constexpr X4 x4;
x4.f(); // 结果为 4

explicit(bool)

在编译期有条件地选择构造函数是否为显式(explicit)。explicit(true) 等价于直接指定 explicit

c++ 复制代码
struct foo {
  // 指定非整数类型(字符串、浮点数等)需要显式构造
  template <typename T>
  explicit(!std::is_integral_v<T>) foo(T) {}
};

foo a = 123; // 正确
foo b = "123"; // 错误:显式构造函数不参与候选(显式说明符求值为 true)
foo c {"123"}; // 正确

立即函数

constexpr 函数类似,但带有 consteval 说明符的函数必须生成常量。这类函数被称为立即函数(immediate functions)

c++ 复制代码
consteval int sqr(int n) {
  return n * n;
}

constexpr int r = sqr(100); // 正确
int x = 100;
int r2 = sqr(x); // 错误:'x' 的值无法用于常量表达式
                 // 若 `sqr` 是 `constexpr` 函数则正确

using enum

将枚举的成员引入当前作用域,提升代码可读性。

之前的写法:

c++ 复制代码
enum class rgba_color_channel { red, green, blue, alpha };

std::string_view to_string(rgba_color_channel channel) {
  switch (channel) {
    case rgba_color_channel::red:   return "red";
    case rgba_color_channel::green: return "green";
    case rgba_color_channel::blue:  return "blue";
    case rgba_color_channel::alpha: return "alpha";
  }
}

改进后的写法:

c++ 复制代码
enum class rgba_color_channel { red, green, blue, alpha };

std::string_view to_string(rgba_color_channel my_channel) {
  switch (my_channel) {
    using enum rgba_color_channel;
    case red:   return "red";
    case green: return "green";
    case blue:  return "blue";
    case alpha: return "alpha";
  }
}

参数包的 Lambda 捕获

按值捕获参数包:

c++ 复制代码
template <typename... Args>
auto f(Args&&... args){
    // 按值捕获:
    return [...args = std::forward<Args>(args)] {
        // ...
    };
}

按引用捕获参数包:

c++ 复制代码
template <typename... Args>
auto f(Args&&... args){
    // 按引用捕获:
    return [&...args = std::forward<Args>(args)] {
        // ...
    };
}

char8_t

提供用于表示 UTF-8 字符串的标准类型。

c++ 复制代码
char8_t utf8_str[] = u8"\u0123";

constinit

constinit 说明符要求变量必须在编译期初始化。

c++ 复制代码
const char* g() { return "dynamic initialization"; }
constexpr const char* f() { return "constant initializer"; }

constinit const char* c = f();  // 正确
constinit const char* d = g();  // 错误:`g` 不是 constexpr,因此 `d` 无法在编译期求值

VA_OPT

辅助支持变长宏,若变长宏非空,则展开为给定的参数;若为空,则不展开任何内容。

c++ 复制代码
#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)
F(a, b, c) // 展开为 f(0, a, b, c)
F()        // 展开为 f(0)

C++20 库特性

文本格式化

标准库新增了编译期检查的字符串格式化库,通过 std::format 实现。也可通过 std::vformat 及其他辅助工具在运行时对动态格式化字符串进行文本格式化。文本格式化遵循指定的规范

std::format 的第一个参数是格式字符串,后续是可变数量的参数。若格式化失败,编译会直接报错:

cpp 复制代码
std::format("{}", 123); // 正确 ------ 返回 "123"
std::format("{} {}", 123); // 错误 ------ 参数数量不足
std::format("{} {}", "Here's a number:", 123); // 正确

基于运行时创建的格式化器格式化字符串:

cpp 复制代码
std::string fmt = "{} {}";
fmt += "{}{}";
std::vformat(fmt, std::make_format_args("Here's a number:", 1, 2, 3));
// 正确 ------ 返回 "Here's a number: 123"

若格式化失败(例如格式字符串无效),std::vformat 会抛出 std::format_error 异常。

格式化自定义类型:

c++ 复制代码
struct fraction {
  int numerator;
  int denominator;
};

template <>
struct std::formatter<fraction> {
  constexpr auto parse(std::format_parse_context& ctx) {
    return ctx.begin();
  }

  auto format(const fraction& f, std::format_context& ctx) const {
    return std::format_to(ctx.out(), "{0:d}/{1:d}", f.numerator, f.denominator);
  }
};

fraction f{1, 2};
std::format("{}", f); // 结果为 "1/2"

概念库

标准库也提供了一系列概念,用于构建更复杂的自定义概念。其中部分概念如下:

核心语言概念

  • same_as - 指定两种类型完全相同。
  • derived_from - 指定某类型派生自另一类型。
  • convertible_to - 指定某类型可隐式转换为另一类型。
  • common_with - 指定两种类型拥有共同类型。
  • integral - 指定某类型为整数类型。
  • default_constructible - 指定某类型的对象可默认构造。

比较概念

  • boolean - 指定某类型可用于布尔上下文。
  • equality_comparable - 指定 operator== 是等价关系。

对象概念

  • movable - 指定某类型的对象可移动和交换。
  • copyable - 指定某类型的对象可拷贝、移动和交换。
  • semiregular - 指定某类型的对象可拷贝、移动、交换且可默认构造。
  • regular - 指定某类型是正则类型 ,即同时满足 semiregularequality_comparable

可调用概念

  • invocable - 指定可调用类型可使用给定的参数类型集进行调用。
  • predicate - 指定可调用类型是布尔谓词。

另见:概念

同步缓冲输出流

为包装的输出流缓冲输出操作,确保同步(即输出不会交错)。

c++ 复制代码
std::osyncstream{std::cout} << "The value of x is:" << x << std::endl;

std::span

span 是容器的视图(即不拥有数据),提供对连续元素组的边界检查访问。由于视图不拥有元素,其构造和拷贝的开销极低------可以简单理解为视图持有指向数据的引用。相比于手动维护指针/迭代器和长度字段,span 将两者封装在单个对象中。

span 可分为动态大小(dynamic-sized)和固定大小(fixed-sized,称为其范围(extent) )两种。固定大小的 span 能受益于编译期边界检查。

span 不会传递常量性(const),因此要构造只读的 span,需使用 std::span<const T>

示例:使用动态大小的 span 打印不同容器中的整数。

c++ 复制代码
void print_ints(std::span<const int> ints) {
    for (const auto n : ints) {
        std::cout << n << std::endl;
    }
}

print_ints(std::vector{ 1, 2, 3 });
print_ints(std::array<int, 5>{ 1, 2, 3, 4, 5 });

int a[10] = { 0 };
print_ints(a);
// 其他容器同理

示例:固定大小的 span 若与容器的范围不匹配,编译会失败。

c++ 复制代码
void print_three_ints(std::span<const int, 3> ints) {
    for (const auto n : ints) {
        std::cout << n << std::endl;
    }
}

print_three_ints(std::vector{ 1, 2, 3 }); // 错误
print_three_ints(std::array<int, 5>{ 1, 2, 3, 4, 5 }); // 错误
int a[10] = { 0 };
print_three_ints(a); // 错误

std::array<int, 3> b = { 1, 2, 3 };
print_three_ints(b); // 正确

// 若需要,可手动构造 span:
std::vector c{ 1, 2, 3 };
print_three_ints(std::span<const int, 3>{ c.data(), 3 }); // 正确:指定指针和长度字段
print_three_ints(std::span<const int, 3>{ c.cbegin(), c.cend() }); // 正确:使用迭代器对

位运算

C++20 新增 <bit> 头文件,提供若干位运算函数,包括 popcount(统计置位位数)。

c++ 复制代码
std::popcount(0u); // 0
std::popcount(1u); // 1
std::popcount(0b1111'0000u); // 4

数学常量

<numbers> 头文件中定义了数学常量,包括圆周率(PI)、自然常数(e)等。

c++ 复制代码
std::numbers::pi; // 3.14159...
std::numbers::e; // 2.71828...

std::is_constant_evaluated

一个谓词函数,在编译期上下文中调用时返回 true,运行时调用时返回 false

c++ 复制代码
constexpr bool is_compile_time() {
    return std::is_constant_evaluated();
}

constexpr bool a = is_compile_time(); // true
bool b = is_compile_time(); // false

std::make_shared 支持数组

c++ 复制代码
auto p = std::make_shared<int[]>(5); // 指向 `int[5]` 的指针
// 或
auto p = std::make_shared<int[5]>(); // 指向 `int[5]` 的指针

字符串的 starts_with 和 ends_with 方法

字符串(及字符串视图)新增 starts_withends_with 成员函数,用于检查字符串是否以指定字符串开头/结尾。

c++ 复制代码
std::string str = "foobar";
str.starts_with("foo"); // true
str.ends_with("baz"); // false

检查关联式容器是否包含元素

集合(set)、映射(map)等关联式容器新增 contains 成员函数,可替代"查找并检查迭代器是否到达末尾"的惯用写法。

c++ 复制代码
std::map<int, char> map {{1, 'a'}, {2, 'b'}};
map.contains(2); // true
map.contains(123); // false

std::set<int> set {1, 2, 3};
set.contains(2); // true

std::bit_cast

一种更安全的方式,将对象从一种类型重新解释为另一种类型。

c++ 复制代码
float f = 123.0;
int i = std::bit_cast<int>(f);

std::midpoint

安全计算两个整数的中点(避免溢出)。

c++ 复制代码
std::midpoint(1, 3); // 结果为 2

std::to_array

将给定的数组/"类数组"对象转换为 std::array

c++ 复制代码
std::to_array("foo"); // 返回 `std::array<char, 4>`
std::to_array<int>({1, 2, 3}); // 返回 `std::array<int, 3>`

int a[] = {1, 2, 3};
std::to_array(a); // 返回 `std::array<int, 3>`

std::bind_front

将前 N 个参数(N 为 std::bind_front 中函数参数后的参数数量)绑定到指定的自由函数、Lambda 表达式或成员函数。

c++ 复制代码
const auto f = [](int a, int b, int c) { return a + b + c; };
const auto g = std::bind_front(f, 1, 1);
g(1); // 结果为 3

统一的容器擦除接口

为字符串、列表、向量、映射等多种 STL 容器提供 std::erase 和/或 std::erase_if 函数。

按值擦除元素使用 std::erase,按谓词条件擦除元素使用 std::erase_if。两个函数均返回被擦除的元素数量。

c++ 复制代码
std::vector v{0, 1, 0, 2, 0, 3};
std::erase(v, 0); // v 变为 {1, 2, 3}
std::erase_if(v, [](int n) { return n == 0; }); // v 仍为 {1, 2, 3}

三路比较辅助函数

为比较结果提供具名判断的辅助函数:

c++ 复制代码
std::is_eq(0 <=> 0); // 结果为 true
std::is_lteq(0 <=> 1); // 结果为 true
std::is_gt(0 <=> 1); // 结果为 false

另见:三路比较

std::lexicographical_compare_three_way

使用三路比较按字典序比较两个范围,并生成适用的最强比较类别类型的结果。

c++ 复制代码
std::vector a{0, 0, 0}, b{0, 0, 0}, c{1, 1, 1};

auto cmp_ab = std::lexicographical_compare_three_way(
    a.begin(), a.end(), b.begin(), b.end());
std::is_eq(cmp_ab); // 结果为 true

auto cmp_ac = std::lexicographical_compare_three_way(
    a.begin(), a.end(), c.begin(), c.end());
std::is_lt(cmp_ac); // 结果为 true

另见:三路比较三路比较辅助函数

std::jthread

一种执行线程(类似 std::thread),会在析构时自动合并(join),且可被发送停止信号。

std::thread 不同(需手动检查线程是否可合并,再调用 join),std::jthread 会通过其析构函数自动尝试合并。

此外,可通过调用 std::jthread::request_stop 或线程的 stop_source 请求 std::jthread 停止:

cpp 复制代码
std::jthread t{
    [](std::stop_token stoken) {
        while (!stoken.stop_requested()) {
            std::this_thread::sleep_for(1s);
        }
    }
};

// 从线程对象发起停止请求:
t.request_stop();
// 或通过 stop_source 发起:
std::stop_source stopSource = t.get_stop_source();
stopSource.request_stop();

std::stop_token 可用于查询线程的停止状态。

安全的整数比较

比较整数(包括不同类型的整数),避免整数转换带来的风险。

cpp 复制代码
-1 > 0U; // 结果为 true(非安全比较,存在符号扩展问题)
std::cmp_greater(-1, 0U); // 结果为 false(安全比较)

std::cmp_equal(0U, 0); // 结果为 true
std::cmp_less_equal(-1, 1U); // 结果为 true

std::in_range<unsigned>(-1); // 结果为 false
std::in_range<char>(999999); // 结果为 false
相关推荐
Championship.23.241 小时前
Linux Top 命令族深度解析与实战指南
java·linux·服务器·top·linux调试
芝士就是力量啊 ೄ೨2 小时前
Python如何编写一个简单的类
开发语言·python
橘子海全栈攻城狮2 小时前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
逻辑驱动的ken2 小时前
Java高频面试考点18
java·开发语言·数据库·算法·面试·职场和发展·哈希算法
MoonBit月兔2 小时前
「Why MoonBit 」第一期——Singularity Note AI 学习助手
开发语言·人工智能·moonbit
木木_王2 小时前
嵌入式Linux学习 | 数据结构 (Day05) 栈与队列详解(原理 + C 语言实现 + 实战实验 + 易错点剖析)
linux·c语言·开发语言·数据结构·笔记·学习
冷雨夜中漫步2 小时前
Claude Code源码分析——Claude Code Agent Loop 详细设计文档
java·开发语言·人工智能·ai
超龄编码人2 小时前
Qt Widgets Designer QTabWidget无法添加布局
开发语言·qt
直奔標竿2 小时前
Java开发者AI转型第二十六课!Spring AI 个人知识库实战(五)——联网搜索增强实战
java·开发语言·人工智能·spring boot·后端·spring