当前位置: 首页 > news >正文

CppCon 2014 学习:CHEAP, SIMPLE, AND SAFE LOGGING USING C++ EXPRESSION TEMPLATES

这段代码定义了一个简单的日志宏 LOG,用来在代码里方便地打印调试信息。

代码细节解析:

#define LOG(msg) \if (s_bLoggingEnabled) \std::cout << __FILE__ << "(" << __LINE__ << "): " << msg << std::endl
  • s_bLoggingEnabled 是一个全局开关,控制是否启用日志输出。
  • __FILE____LINE__ 是预定义宏,分别表示当前文件名和当前代码行号。
  • msg 是传入的日志内容,利用 C++ 的流式输出(<<)格式,类型安全且灵活
  • 宏展开后,相当于:
    if (s_bLoggingEnabled)std::cout << "当前文件名(行号): " << msg << std::endl;
    

使用示例:

void foo() {std::string file = "blah.txt";int error = 123;LOG("Read failed: " << file << " (" << error << ")");
}

打印结果示例:

test.cpp(5): Read failed: blah.txt (123)

总结:

  • 方便:只需写 LOG(...),无需重复写打印格式代码。
  • 类型安全:利用 C++ 流操作符,无需手动格式化字符串。
  • 带上下文:自动打印文件名和代码行号,方便定位问题。
  • 可控开关:通过 s_bLoggingEnabled 动态开启或关闭日志。

这段是对之前的 LOG 宏经过预处理(pre-processing)后的展开效果:

解释

  • 预处理器把 LOG("Read failed: " << file << " (" << error << ")"); 展开成了:
    if (s_bLoggingEnabled)std::cout << "foo.cpp" << "(" << 53 << "): " << "Read failed: " << file << " (" << error << ")"<< std::endl;
    
  • 其中 "foo.cpp" 是当前文件名(__FILE__),53 是代码行号(__LINE__)。
  • fileerror 是变量,仍保持流式输出,类型安全。

作用

  • 宏的作用就是把日志输出封装成一条语句,自动附加文件名和行号,方便调试定位。
  • 预处理后你看到的就是具体的 if 判断加标准输出语句。

你给的这段汇编代码是编译器对含有日志宏 LOG(...) 的 C++ 代码生成的机器指令示例,展示了日志语句的具体实现细节。

void foo() {string file = "blah.txt";int error = 123;...movb  g_bLogging(%rip), %altestb %al, %alje  ..B2.14movl  $_ZSt4cout, %edimovl  $.L_2__STRING.3, %esicall  ostream& operator<<(ostream&, char const*)movq  %rax, %rdimovl  $.L_2__STRING.0, %esicall  ostream& operator<<(ostream&, char const*)movq  %rax, %rdimovl  $19, %esicall  ostream::operator<<(int)...movq  %rax, %rdimovl  $ostream& endl(ostream&), %esicall  ostream::operator<<(ostream& (*)(ostream&))
}

解析:

  • movb g_bLogging(%rip), %al
    取日志开关变量的值到寄存器 %al
  • testb %al, %al
    测试 %al(即判断是否启用日志)。
  • je ..B2.14
    如果日志未启用,跳过日志输出代码。
  • 接下来是一系列调用 std::ostream 的输出操作:
    • operator<< 依次输出文件名、行号、消息内容等。
    • 每次调用后 %rax 保存返回的 ostream&,作为下次调用的第一个参数(链式调用)。
    • 最后调用输出流的 endl,刷新输出缓冲。
  • 编译参数包括 -O3(优化),说明生成的汇编是经过高优化的。
  • 总体代码约33条指令,10次函数调用,比较高效。

总结:

  • 通过汇编代码你可以看到,日志开关检查是条件跳转,性能代价低
  • 每条日志输出通过调用标准流操作符实现,保持类型安全和灵活性。
  • 编译器对日志宏生成的代码做了优化,但调用开销仍在(函数调用多)。
  • 汇编代码体现了日志宏背后的底层实现细节。

你指出了日志代码带来的性能和优化问题,总结得很准确:

问题:

  • 日志相关的指令多,尤其是流操作符和条件判断,导致代码臃肿。
  • 这些指令会阻碍编译器的优化,因为它们可能引入不可预测的控制流和函数调用。
  • 影响指令缓存(icache)性能,代码体积变大,缓存命中率降低。
  • 但我们又想保持日志的速度、类型安全和使用方便

目标:

  • 减少调用日志时的指令数量,即代码体积更小。
  • 仍然保持日志输出速度快,不会显著拖慢程序。
  • 保持C++流操作符的类型安全
  • 使用上仍然简洁方便
    这就是日志设计中的经典权衡点。接下来,可以考虑的优化方案包括:
  • 使用**零开销日志(zero-overhead logging)**技巧,编译期剔除无用日志。
  • 设计更轻量的日志API,避免不必要的函数调用。
  • 利用宏和模板元编程在编译期计算是否启用日志。
  • 结合缓冲区或异步日志减少运行时开销。

是一种通过“表达式模板”(Expression Templates)技术解决日志调用开销的方案,核心思路如下:

核心问题

  • 保持 流式(operator<<)接口 的优雅和类型安全
  • 但避免 每次日志调用都产生多次函数调用operator<< 的开销)

解决方案:表达式模板(Expression Templates)

  • 利用 C++ 运算符重载,在编译期把日志表达式 封装成一个类型(表达式树)
  • 日志表达式如 "Read failed: " << file << " (" << error << ")"
    不在运行时一步步调用 operator<<,而是先变成一个编译期的表达式对象
  • 这样可以在运行时 一次性处理整个表达式,减少函数调用次数和指令数量

类比示例

  • 矩阵计算:
    Matrix D = A + B * C;
    编译器用表达式模板避免生成临时矩阵,多做合并优化。
  • 条件查询:
    polygons.Find(VERTICES == 4 && WIDTH >= 20);
    编译时构建查询条件表达式。
  • 日志表达式:
    LOG("Read failed: " << file << " (" << error << ")");
    通过表达式模板,编译时构造表达式树,运行时统一输出。

总结

  • 通过表达式模板,可以实现 零运行时开销的流式日志
  • 保持 类型安全方便易用的接口
  • 大幅减少日志调用时的指令数量,提高性能和优化空间

这是用表达式模板实现日志系统的一部分代码,解析如下:

代码结构

#define LOG(msg) \if (s_bLoggingEnabled) \(Log(__FILE__, __LINE__, LogData<None>() << msg))
template<typename List>
struct LogData {typedef List type;List list;
};
struct None { };

说明

  • LOG(msg) 宏:
    • 检查日志开关 s_bLoggingEnabled
    • 创建一个空的 LogData<None>() 对象(表示空的日志数据列表)
    • 利用重载的 operator<<msg 添加进这个 LogData,构建日志表达式
    • 把文件名、行号和构造的日志数据传给 Log() 函数
  • LogData<List> 模板结构体:
    • 作为表达式模板的核心,保存“日志消息链”(这里用 List 代表消息列表或表达式树)
    • 通过模板递归展开,实现链式拼接日志内容
  • None
    • 表示初始的空日志数据类型

整体作用

  • 利用模板和运算符重载,将日志消息“拼接”成一个类型安全的表达式模板结构
  • 日志内容在运行时才调用 Log() 输出,之前只构建表达式类型,减少多次函数调用开销
  • 保持了流式接口的使用习惯,同时允许编译期优化

这段代码是实现 LogData 表达式模板的关键 operator<<,它实现了日志数据的链式拼接。具体分析如下:

代码内容

template<typename Begin, typename Value>
LogData<std::pair<Begin&&, Value&&>> operator<<(LogData<Begin>&& begin, Value&& v) noexcept {return {{ std::forward<Begin>(begin.list), std::forward<Value>(v) }};
}

解释

  • 模板参数
    • Begin:表示已有的日志数据类型(表达式模板中的“前半部分”)
    • Value:本次要加入的新日志值类型(右操作数)
  • 参数
    • begin:右值引用,表示已有的 LogData<Begin> 对象(即当前已有的日志链)
    • v:要追加的值(如字符串、变量等)
  • 返回类型
    • 返回一个新的 LogData,其模板参数是一个 std::pair,组合了之前的 begin.list 和新值 v
    • 这样就形成了一个链表结构,每个节点都包含前面的表达式和当前新值
  • 实现
    • 使用 std::forward 完美转发参数,保证传递值的引用性质(左值/右值)
    • 通过花括号初始化 std::pairLogData 对象

作用

  • 每次执行 operator<< 都是把一个新的值追加到已有的 LogData 链表中,形成嵌套的 std::pair 类型链
  • 这个链条在编译时展开,运行时可以一次性遍历输出全部日志内容
  • 保持类型安全无额外函数调用开销

举例

调用示例:

auto data = LogData<None>() << "Error: " << errorCode;
  • 第一次 << "Error:",把 "Error:" 包装进 LogData<std::pair<None, const char*>>
  • 第二次 << errorCode,把 errorCode 和之前链表继续包成新的 LogData<std::pair<std::pair<None, const char*>, int>>
    #结构解析:
LOG("Read failed: " << file << " (" << error << ")");

在宏展开、运算符重载作用下,会构建出如下嵌套类型结构:

LogData<pair<pair<pair<pair<pair<None,char const*>,         // "Read failed: "string const&>,         // filechar const*>,             // " ("int const&>,                // errorchar const*>                  // ")"
>

这是怎么产生的?

每次你使用 <<,都会走这段代码:

template<typename Begin, typename Value>
LogData<std::pair<Begin&&, Value&&>> operator<<(LogData<Begin>&& begin, Value&& v);

这就把“前一个表达式”和“当前要加入的值”合并为一个新的 std::pair
所以会形成链式嵌套结构,每个新 LogData 都把前一个表达式 Begin 作为链表的头节点。

为什么这样做?

这是表达式模板的经典技巧,优点如下:

优点描述
编译期结构构建表达式在编译期以类型嵌套的形式构建完成,无运行时拼接开销
类型安全所有内容在编译期就明确了类型(string, int 等),无字符串格式化出错问题
无需临时变量不会像 stringstream 那样创建临时对象,减小运行时开销
高性能日志输出编译器可以优化掉未启用日志的所有代码路径(零开销)

总结并详细解释一下你提供的 LOG() 实现和其递归原理:

整体结构

你写的是一个 表达式模板日志系统的运行期输出逻辑,它会:

  1. 在编译期 使用模板构建日志消息的链式结构(嵌套 std::pair
  2. 在运行期 递归展开这个链并输出每一部分

代码详解

Log()

template<typename TLogData>
void Log(const char* file, int line, TLogData&& data) noexcept __attribute__((__noinline__)) {std::cout << file << "(" << line << "): ";Log_Recursive(std::cout, std::forward<typename TLogData::type>(data.list));std::cout << std::endl;
}
  • TLogData 是一个 LogData<...> 类型
  • TLogData::type 是嵌套的 std::pair 类型(日志表达式链)
  • data.list 是实际数据链
  • 使用 Log_Recursive 递归遍历整个链并输出
  • __noinline__:避免编译器内联该函数(保持调试时清晰)

Log_Recursive():递归版本

template<typename TLogDataPair>
void Log_Recursive(std::ostream& os, TLogDataPair&& data) noexcept {Log_Recursive(os, std::forward<typename TLogDataPair::first_type>(data.first));os << std::forward<typename TLogDataPair::second_type>(data.second);
}
  • 遍历 pair<前面内容, 当前值>
  • 先递归打印 .first(链表的前面)
  • 然后输出 .second(当前这一层的值)

Log_Recursive():终止版本

inline void Log_Recursive(std::ostream& os, None) noexcept
{ }
  • 终止条件,当到达最前端 None 类型时不再输出

打印流程图

对于:

LOG("Read failed: " << file << " (" << error << ")");

最终输出类似:

main.cpp(42): Read failed: blah.txt (123)

内部运行时递归调用顺序大致如下:

Log_Recursive(..., pair<..., ")">)└── Log_Recursive(..., pair<..., error>)└── Log_Recursive(..., pair<..., " (">)└── ...└── Log_Recursive(..., None)

每层输出一个值,直到链表遍历完成。

优点总结

优点描述
零运行时开销(禁用时)宏判断 + 模板链式结构避免执行
编译时类型检查所有拼接内容都必须合法 << 运算符
可扩展性强(可加入日志策略)如 mock、profile、日志级别
性能优越优化后无多余函数调用

详细解释一下这段代码的目的和原理,尤其是 处理 std::endl 这样的流操作符(stream manipulators)

问题背景:为什么要特别处理 manipulators?

当你这样写:

LOG("Count: " << count << std::endl);

其中 std::endl 不是普通的值,而是一个 函数指针,它的签名是:

std::ostream& endl(std::ostream&);

这叫做 stream manipulator,像 std::endlstd::flushstd::hex 都是这样。
如果不专门处理它,表达式模板机制就会在编译时出错(类型不匹配)。

解决方案:函数指针偏特化模板

类型定义:

typedef std::ostream& (*PfnManipulator)(std::ostream&);

这就是 manipulator 的类型,本质上是一个函数指针,指向返回 ostream& 并接受一个 ostream& 参数的函数。

operator<< 重载:

template<typename Begin>
LogData<std::pair<Begin&&, PfnManipulator>> operator<<(LogData<Begin>&& begin, PfnManipulator pfn) noexcept {return {{ std::forward<Begin>(begin.list), pfn }};
}
  • Begin 是前面链式结构的日志数据类型
  • pfnstd::endl 这类操作符的函数指针
  • 继续构建嵌套 std::pair

最终效果

这段代码允许如下语法合法且能被正确处理:

LOG("Result is: " << result << std::endl);

Log_Recursive() 函数中,这个 pfn 也能被正确递归处理为:

os << pfn;  // 等效于 os << std::endl;

总结

项目内容
解决的问题支持 std::endl 等流操作符
处理方式ostream& (*)(ostream&) 类型专门提供 operator<< 重载
兼容性完整支持任意流拼接表达式
类型安全编译期确保所有内容都能插入 ostream

这一段代码处理的是 字符串字面量优化(String Literal Optimization)。我们来一步一步拆解并说明这段代码的意图和作用。

问题背景

当你写:

LOG("Error: " << error);

其中的 "Error: " 是一个 字符串字面量(string literal),它的类型是:

const char[8]  // 对于 "Error: " 来说是8(含 null terminator)

这个类型和 const char* 不一样,所以没有合适的 operator<< 重载时会导致模板匹配失败。

解决方案:模板重载接受 const char (&sz)[n]

template<typename Begin, size_t n>
LogData<std::pair<Begin&&, const char*>> operator<<(LogData<Begin>&& begin, const char (&sz)[n]) noexcept {return {{ std::forward<Begin>(begin.list), sz }};
}

参数解释:

  • Begin:前面构建好的链式日志数据结构
  • sz引用类型的字符数组,也就是 string literal,如 "Error"const char (&)[6]
  • n:模板参数,用来匹配任意长度的字符串字面量

返回值:

  • 返回一个新的 LogData,在已有链条的基础上添加一个 const char* 类型
    这就将 "Error" 类型从 const char[6] 转换成了 const char* 存储,更加轻量,也统一了类型。

优势

优点描述
支持字符串字面量不支持会导致编译错误
避免每次都拷贝字符串字面量的内容只存指针,效率高
统一类型为 const char*简化后续递归处理逻辑
零开销类型转换编译期完成,不增加运行时负担

示例用法

LOG("Error at line: " << lineNum << " in file: " << filename);

对于上面这段,模板重载会自动识别 "Error at line: "" in file: " 为 string literal,匹配这个特化模板,转成 const char*

总结

你看到的这一段代码实现的是:

专门支持 "字符串字面量" 这种特殊的数组类型,并将其优化为 const char* 来存储,提高日志系统的灵活性和效率。

你这段汇编和其后的模板展开反映的是 高性能、低指令开销的日志系统实现方式

场景回顾

你在使用这样的日志语句:

LOG("Read failed: " << file << " (" << error << ")");

其中 LOG 是一个宏,最终展开为对 Log(...) 的一次调用,使用了 表达式模板(expression templates) 技术来延迟表达式求值、减少运行时指令。

汇编代码解释

movb g_bLogging(%rip), %al      ; 加载全局布尔变量 s_bLoggingEnabled
testb %al, %al                  ; 测试它是否为真
je    ..B6.7                    ; 如果为假,跳转(不执行日志)
movb $0, (%rsp)                 ; 临时栈处理(非关键)
movl $.L_2__STRING.4, %ecx      ; 加载字符串字面量指针(如 " (" )
movl $.L_2__STRING.3, %edi      ; 加载另一个字符串(如 "Read failed: ")
movl $40, %esi                  ; 加载行号
lea   128(%rsp), %r9            ; 设置临时地址作为参数
call Log<...>                   ; 调用唯一的 Log 模板函数实例

模板展开(推导出的 LogData)

Log<pair<pair<pair<pair<pair<None,char const*       // "Read failed: ">,string const&         // file>,char const*               // " (">,int const&                   // error>,char const*                     // ")"
>>

这是你通过 << 运算符链式拼接出来的日志数据表达式,模板在编译期就构建好了这些类型。
最终传给唯一的 Log(...) 函数,运行时只需一次函数调用(pimp’d function call)即可完成整条日志输出。

优点

优点说明
编译期表达式组合<< 构建表达式的结构体,不执行实际操作
运行期延迟调用只有一次 Log 函数调用(模板实例化)
少量汇编指令这里只用了 9 条汇编指令来构造参数和调用
保留语义完整性LOG(...) 保持流式语法,且类型安全
优化友好编译器能轻松做内联或省略,性能极高

理解重点

你看到的这一切说明:表达式模板技术允许你写出非常干净的代码,而编译器又能生成非常高效的汇编。 它兼顾了:

  • 语法简洁
  • 类型安全
  • 运行时性能
  • 最小的指令和函数调用开销

总结提到的是:使用表达式模板的日志系统,在性能和编译优化方面的巨大优势。下面逐点解释:

SUMMARY 理解

• Expression templates solution

表达式模板方案
使用表达式模板技术(即 << 拼接被延迟到编译期生成类型结构),实现日志消息的构建。这种方式不在日志调用处做字符串拼接或格式化,而是传递一个类型安全的结构(LogData<...>)到一个统一的日志处理函数。

• Reduced instructions at call site by 73% (33 → 9)

调用处的汇编指令减少了 73%(从 33 条降到 9 条)
传统的日志实现使用大量的 operator<<,每次 << 都是函数调用。表达式模板把这些函数调用“挪”到编译期,只留下最终调用 Log(...) 的一次函数调用。
这极大地提升了性能,特别是在嵌入式或高频调用场景下。

• Mo’ args, mo’ savings

参数越多,节省越大
传统日志系统每多一个参数,就多一到两个函数调用。而表达式模板只构建更复杂的类型结构,运行时开销不变。你写:

LOG("Read failed: " << file << " (" << error << ") at offset " << offset << ", reason: " << reason);

无论参数多少,运行时也就一两次函数调用。结果是:
参数越多,节省越多!

总结一句话

表达式模板让你保留了优雅的写法,却几乎不付出运行时代价,是一种兼顾语义表达极致性能的高级技巧。

可变参数模板(Variadic Template)日志系统 的完整代码版本,并附带了详细注释,帮助你理解其结构与作用:

完整代码:Variadic Template Logging

#include <iostream>
#include <string>
bool s_bLoggingEnabled = true;  // 控制是否启用日志
// 提前声明递归函数模板
template <typename T, typename... Args>
void Log_Recursive(const char* file, int line, std::ostream& os, T first, const Args&... rest);
inline void Log_Recursive(const char* file, int line, std::ostream& os);  // 终止版本声明
// 定义 LOG 宏,用于自动捕获 __FILE__ 和 __LINE__,并转发可变参数到 Log_Variadic
#define LOG(...) Log_Variadic(__FILE__, __LINE__, __VA_ARGS__)
// 主日志函数模板,接受任意数量和类型的参数
template <typename... Args>
void Log_Variadic(const char* file, int line, const Args&... args) {if (!s_bLoggingEnabled) return;                 // 如果日志未启用,直接返回std::cout << file << "(" << line << "): ";      // 打印文件名和行号Log_Recursive(file, line, std::cout, args...);  // 展开参数并输出std::cout << std::endl;                         // 换行
}
// 递归模板函数:处理一个参数,然后递归处理剩余的参数
template <typename T, typename... Args>
void Log_Recursive(const char* file, int line, std::ostream& os, T first, const Args&... rest) {os << first;                             // 打印当前参数Log_Recursive(file, line, os, rest...);  // 递归处理剩余参数
}
// 递归终止函数:不做任何事(没有参数时)
inline void Log_Recursive(const char* file, int line, std::ostream& os) {// 什么都不做,递归终止
}

使用示例

// 示例
int main() {std::string file = "config.txt";int errorCode = 404;LOG("Failed to read file: ", file, ", error code: ", errorCode);
}

输出示例:

/home/xiaqiu/test/CppCon/day46/code/main.cpp(40): Failed to read file: config.txt, error code: 404

特点总结

特性说明
类型安全使用模板展开参数,支持任意类型(只要可输出到 std::ostream
语法简洁printf 相比更现代、更安全,且无需格式字符串
可扩展可添加如时间戳、线程 ID、颜色等功能
条件编译可以结合宏或 s_bLoggingEnabled 控制开关

下面这段代码(专门用于优化字符串字面量处理)的完整可运行示例,包含基础 LogData 类型、递归输出逻辑、宏定义等内容。这个版本模拟了使用 表达式模板风格的日志系统,特别是你提到的 operator<< 针对 const char[N] 的特化处理。

完整示例代码:支持字符串字面量优化的日志系统

#include <iostream>
#include <string>
#include <utility>
#include <type_traits>
// 控制是否启用日志
bool s_bLoggingEnabled = true;
// 基础类型 None,用于终止递归链表结构
struct None { };
// 表达式模板用的包装类型 LogData
template<typename List>
struct LogData {using type = List;List list;
};
// 重载 operator<<:通用版本,将任意类型“连接”到 LogData 链中
template<typename Begin, typename Value>
LogData<std::pair<Begin&&, Value&&>>
operator<<(LogData<Begin>&& begin, Value&& v) noexcept {return {{ std::forward<Begin>(begin.list), std::forward<Value>(v) }};
}
// 特化版本:优化 const char[N] 字符串字面量的拼接(避免构造 std::string)
template<typename Begin, size_t n>
LogData<std::pair<Begin&&, const char*>>
operator<<(LogData<Begin>&& begin, const char (&sz)[n]) noexcept {return {{ std::forward<Begin>(begin.list), sz }};
}
// 操作流控制符(如 std::endl)的版本
using PfnManipulator = std::ostream& (*)(std::ostream&);
template<typename Begin>
LogData<std::pair<Begin&&, PfnManipulator>>
operator<<(LogData<Begin>&& begin, PfnManipulator pfn) noexcept {return {{ std::forward<Begin>(begin.list), pfn }};
}
// 宏:包装日志调用,自动注入文件名和行号
#define LOG(msg) \if (s_bLoggingEnabled) \Log(__FILE__, __LINE__, LogData<None>() << msg)
// 主日志函数(展开表达式链)
template<typename TLogData>
void Log(const char* file, int line, TLogData&& data) noexcept {std::cout << file << "(" << line << "): ";Log_Recursive(std::cout, std::forward<typename TLogData::type>(data.list));std::cout << std::endl;
}
// 递归打印表达式链
template<typename TLogDataPair>
void Log_Recursive(std::ostream& os, TLogDataPair&& data) noexcept {Log_Recursive(os, std::forward<typename TLogDataPair::first_type>(data.first));os << std::forward<typename TLogDataPair::second_type>(data.second);
}
// 递归终止函数
inline void Log_Recursive(std::ostream& os, None) noexcept {// Do nothing
}
// 示例
int main() {std::string filename = "data.txt";int errCode = 42;LOG("Error opening " << filename << ": code " << errCode << std::endl);
}

输出示例

如果编译并运行,会输出类似:

main.cpp(87): Error opening data.txt: code 42

特点说明

  • 表达式模板 LogData + operator<< 允许将多个元素拼接成“延迟求值”的链表结构;
  • 字符串字面量版本的 operator<< 避免将其视为模板推导中需要额外构造的 std::string,提升效率;
  • 支持任意类型拼接、流操作符(如 std::endl);
  • 所有输出逻辑最终只在 Log() 函数中统一处理,便于拦截、替换输出流(比如写入文件)或增加 profiling。

相关文章:

  • 《矛盾论》可以带给我们什么?
  • Gradle依赖管理全面指南:从基础到高级实践
  • 第一章 1.The Basic (CCNA)
  • Lua和JS的垃圾回收机制
  • 安全月报 | 傲盾DDoS攻击防御2025年5月简报
  • 【QT】认识QT
  • 帝可得 - 设备管理
  • 【C/C++】初步了解享元模式
  • 小黑一步步探索大模型应用:langchain中AgentExecutor的call方法初探demo(智能体调用)
  • React从基础入门到高级实战:React 高级主题 - React 微前端实践:构建可扩展的大型应用
  • AtCoder Beginner Contest 408(ABCDE)
  • 机器学习——随机森林算法
  • [蓝桥杯]上三角方阵
  • 5. Qt中.pro文件(1)
  • easylogger的移植使用
  • 在linux系统上搭建git服务器(ssh协议)
  • 【理解软件开发中的“向后兼容“与“向前兼容“】
  • 每日算法-250603
  • 龙虎榜——20250603
  • C语言学习—数据类型20250603
  • 视频网站点击链接怎么做/seo排名优化推广教程
  • 中国合伙人2做的什么网站/南宁百度推广代理公司
  • 专做腰带的网站/济南百度
  • 吉安哪里做网站/电商运营培训课程
  • 家具设计/郑州专业seo哪家好
  • 国外简约企业网站/网络推广渠道都有哪些