文章目录
-
- [1. C语言基础回顾 -> C++过渡](#1. C语言基础回顾 -> C++过渡)
-
- [1.1 变量、类型、控制流](#1.1 变量、类型、控制流)
- [1.2 函数与函数指针(理解PluginAPI的关键!)](#1.2 函数与函数指针(理解PluginAPI的关键!))
-
- [1.2.1 普通函数](#1.2.1 普通函数)
- [1.2.2 函数指针:把函数当作变量传递](#1.2.2 函数指针:把函数当作变量传递)
- [1.3 struct与typedef -> class的进化](#1.3 struct与typedef -> class的进化)
-
- [1.3.1 C语言的struct](#1.3.1 C语言的struct)
- [1.3.2 C语言的typedef](#1.3.2 C语言的typedef)
- [1.3.3 从struct到class](#1.3.3 从struct到class)
- [1.4 C字符串char* vs C++ std::string](#1.4 C字符串char* vs C++ std::string)
-
- [1.4.1 C风格字符串(char*)](#1.4.1 C风格字符串(char*))
- [1.4.2 C++的std::string](#1.4.2 C++的std::string)
- [1.5 内存模型:栈 vs 堆](#1.5 内存模型:栈 vs 堆)
- [2. 类与对象](#2. 类与对象)
-
- [2.1 class vs struct](#2.1 class vs struct)
- [2.2 构造函数与析构函数](#2.2 构造函数与析构函数)
- [2.3 拷贝构造与移动构造](#2.3 拷贝构造与移动构造)
- [2.4 public / private / protected](#2.4 public / private / protected)
- [2.5 虚函数与纯虚函数(接口)](#2.5 虚函数与纯虚函数(接口))
- [2.6 this指针](#2.6 this指针)
- [2.7 静态成员函数](#2.7 静态成员函数)
- [3. RAII与智能指针](#3. RAII与智能指针)
-
- [3.1 RAII原则详解](#3.1 RAII原则详解)
- [3.2 项目中最精彩的RAII实例](#3.2 项目中最精彩的RAII实例)
- [3.3 std::unique_ptr ------ 独占所有权](#3.3 std::unique_ptr —— 独占所有权)
- [3.4 std::shared_ptr ------ 共享所有权(引用计数)](#3.4 std::shared_ptr —— 共享所有权(引用计数))
- [3.5 std::weak_ptr ------ 解决循环引用](#3.5 std::weak_ptr —— 解决循环引用)
- [3.6 shared_mutex ------ 读写锁的RAII包装](#3.6 shared_mutex —— 读写锁的RAII包装)
- [4. 模板与泛型编程](#4. 模板与泛型编程)
-
- [4.1 函数模板](#4.1 函数模板)
- [4.2 类模板](#4.2 类模板)
- [4.3 项目中的TSingleton详解](#4.3 项目中的TSingleton详解)
- [4.4 模板的"代码膨胀"问题](#4.4 模板的"代码膨胀"问题)
- [5. Lambda表达式与std::function](#5. Lambda表达式与std::function)
-
- [5.1 Lambda语法](#5.1 Lambda语法)
- [5.2 捕获列表](#5.2 捕获列表)
- [5.3 std::function ------ 类型擦除](#5.3 std::function —— 类型擦除)
- [5.4 Lambda用于异步编程](#5.4 Lambda用于异步编程)
- [6. C++17/20核心新特性](#6. C++17/20核心新特性)
-
- [6.1 auto类型推导](#6.1 auto类型推导)
- [6.2 结构化绑定(Structured Bindings,C++17)](#6.2 结构化绑定(Structured Bindings,C++17))
- [6.3 std::optional(C++17)](#6.3 std::optional(C++17))
- [6.4 std::variant(C++17)](#6.4 std::variant(C++17))
- [6.5 if constexpr(C++17)](#6.5 if constexpr(C++17))
- [6.6 std::jthread(C++20)](#6.6 std::jthread(C++20))
- [6.7 std::span(C++20)](#6.7 std::span(C++20))
- [7. Move语义](#7. Move语义)
-
- [7.1 左值、右值、将亡值](#7.1 左值、右值、将亡值)
- [7.2 为什么需要移动语义](#7.2 为什么需要移动语义)
- [7.3 std::move](#7.3 std::move)
- [7.4 std::forward ------ 完美转发](#7.4 std::forward —— 完美转发)
- [7.5 移动构造/移动赋值与 noexcept](#7.5 移动构造/移动赋值与 noexcept)
- [8. 其他关键知识点](#8. 其他关键知识点)
-
- [8.1 enum class(C++11)](#8.1 enum class(C++11))
- [8.2 namespace](#8.2 namespace)
- [8.3 const与constexpr](#8.3 const与constexpr)
- [8.4 = delete / = default](#8.4 = delete / = default)
- [8.5 mutable](#8.5 mutable)
- [8.6 extern "C" ------ 为什么PluginAPI需要它](#8.6 extern "C" —— 为什么PluginAPI需要它)
- [8.7 多线程基础:项目中使用的同步原语](#8.7 多线程基础:项目中使用的同步原语)
-
- [std::atomic ------ 无锁的原子操作](#std::atomic —— 无锁的原子操作)
- [std::mutex / std::lock_guard / std::unique_lock](#std::mutex / std::lock_guard / std::unique_lock)
- [std::condition_variable ------ 线程间通信](#std::condition_variable —— 线程间通信)
- [std::shared_mutex ------ 读写锁(C++17)](#std::shared_mutex —— 读写锁(C++17))
1. C语言基础回顾 -> C++过渡
1.1 变量、类型、控制流
C++继承了C语言的基本类型系统和控制流,如果你学过任何编程语言,这部分会很熟悉。
cpp
// === 基本类型 ===
int age = 25; // 整数(通常4字节)
float pi = 3.14f; // 单精度浮点数
double e = 2.71828; // 双精度浮点数(更精确)
char c = 'A'; // 单个字符
bool ok = true; // 布尔值(C++独有,C语言没有bool类型)
// === 数组 ===
int scores[5] = {90, 80, 70, 60, 50}; // 固定大小数组
// === 控制流 ===
// if-else
if (age >= 18) {
// 成年
} else {
// 未成年
}
// for 循环
for (int i = 0; i < 5; i++) {
// 循环5次
}
// while 循环
while (ok) {
// 当ok为true时循环
}
项目中的实际应用 (src/main.cpp:124-126):
cpp
// 实际项目中的类型和控制流
int main(int argc, char **argv) { // argc=参数个数, argv=参数值数组
std::string name; // C++字符串类型
bool verbose; // C++布尔类型
// ...
if (use_sse_server->count() > 0) { // if条件判断
transport = std::make_shared<...>();
} else if (use_httpstream_server->count() > 0) {
transport = std::make_shared<...>();
} else {
transport = std::make_shared<...>();
}
}
1.2 函数与函数指针(理解PluginAPI的关键!)
1.2.1 普通函数
cpp
// 函数声明:返回类型 函数名(参数列表)
int add(int a, int b) {
return a + b;
}
// 调用
int result = add(3, 5); // result = 8
1.2.2 函数指针:把函数当作变量传递
这是理解PluginAPI的核心。想象一下:你不仅能调用函数,还能把"函数的地址"存到一个变量里,需要时通过这个变量调用函数。
cpp
// 定义一个函数指针类型:指向"返回int、接受两个int参数"的函数
int (*funcPtr)(int, int);
// 让它指向add函数
funcPtr = &add; // 或者简写 funcPtr = add;
// 通过函数指针调用(两种写法等价)
int r1 = funcPtr(3, 5); // 直接用
int r2 = (*funcPtr)(3, 5); // 解引用后用
为什么需要函数指针? 看这个场景:你的Server框架不知道插件里有什么函数,但它需要调用它们。函数指针就是"函数的契约"------你告诉我函数的地址,我就能调用。
项目中的实际应用 (src/interface/PluginAPI.h:38,69-83):
这是整个项目最重要的数据结构,请仔细看:
cpp
// PluginAPI.h:38 - 定义一个函数指针类型
// "void (*)(const char*, const char*)" 意思是:
// 指向一个返回void、接受两个const char*参数的函数的指针
typedef void (*ClientNotificationCallback)(const char* pluginName, const char* notification);
// PluginAPI.h:69-83 - 插件API结构体,里面全是函数指针!
typedef struct {
const char* (*GetName)(); // 指向返回const char*的函数
const char* (*GetVersion)(); // 同上
PluginType (*GetType)(); // 指向返回PluginType的函数
int (*Initialize)(); // 指向返回int的函数
char* (*HandleRequest)(const char* request); // 指向处理请求的函数
void (*Shutdown)(); // 指向无返回值的函数
int (*GetToolCount)(); // 获取工具数量
const PluginTool* (*GetTool)(int index); // 获取指定索引的工具
int (*GetPromptCount)();
const PluginPrompt* (*GetPrompt)(int index);
int (*GetResourceCount)();
const PluginResource* (*GetResource)(int index);
NotificationSystem* notifications; // 不是函数指针,是数据指针
} PluginAPI;
设计哲学:PluginAPI是一个"虚函数表"的手工实现。每个字段是一个函数指针,插件作者必须提供这些函数的实现。框架通过这个结构体知道"这个插件有什么能力",然后统一调用。
插件如何实现 (plugins/weather/Weather.cpp:197-210):
cpp
// 插件把实现了的函数地址填进结构体
static PluginAPI plugin = {
GetNameImpl, // GetName字段指向GetNameImpl函数
GetVersionImpl, // GetVersion字段指向GetVersionImpl函数
GetTypeImpl,
InitializeImpl,
HandleRequestImpl,
ShutdownImpl,
GetToolCountImpl,
GetToolImpl,
nullptr, // GetPromptCount -- 这个插件没有Prompt,填nullptr
nullptr, // GetPrompt
nullptr, // GetResourceCount
nullptr // GetResource
};
当Server调用plugin->GetName()时,实际上执行的是GetNameImpl()。这就是C语言实现"多态"的方式,而C++的virtual关键字是这种模式的语法糖和自动化。
1.3 struct与typedef -> class的进化
1.3.1 C语言的struct
cpp
// C风格:数据聚合
struct Point {
int x;
int y;
};
// 使用时必须写struct关键字(C语言),C++中可省略
struct Point p;
p.x = 10;
p.y = 20;
1.3.2 C语言的typedef
cpp
// typedef:给类型取别名
typedef unsigned long size_t; // size_t就是unsigned long
typedef void (*Callback)(const char*); // Callback是一个函数指针类型
// 使用
size_t length = 100;
Callback myFunc = someFunction;
项目实例 (src/interface/PluginAPI.h:38,40-67):
cpp
// typedef 给函数指针类型取别名(行38)
typedef void (*ClientNotificationCallback)(const char*, const char*);
// typedef 定义枚举(行40-44)
typedef enum {
PLUGIN_TYPE_TOOLS = 0,
PLUGIN_TYPE_PROMPTS = 1,
PLUGIN_TYPE_RESOURCES = 2
} PluginType; // 以后直接用 PluginType,不需要写 enum
// typedef 定义结构体(行46-50)
typedef struct {
const char* name;
const char* description;
const char* inputSchema;
} PluginTool; // 直接用 PluginTool,不需要 struct 关键字
1.3.3 从struct到class
C++的class本质上是结构体的增强版:
| 特性 | C struct | C++ struct | C++ class |
|---|---|---|---|
| 数据成员 | 有 | 有 | 有 |
| 成员函数 | 无 | 有 | 有 |
| 默认访问权限 | 公开 | 公开 | 私有 |
| 继承 | 无 | 有 | 有 |
| 构造/析构函数 | 无 | 有 | 有 |
cpp
// C++ struct:默认public
struct MyStruct {
int data; // 默认public
void doSomething() { } // 可以有成员函数
};
// C++ class:默认private
class MyClass {
int data; // 默认private,外部无法访问!
public:
void doSomething() { } // 需要显式声明public
};
1.4 C字符串char* vs C++ std::string
这是C/C++初学者最容易出错的地方,也是本项目混合使用的区域。
1.4.1 C风格字符串(char*)
cpp
// C字符串的本质:以'\0'结尾的字符数组
char greeting[] = "Hello"; // 实际存储:'H','e','l','l','o','\0'
const char* name = "World"; // 指向字符串常量的指针
// C字符串操作(需要 #include <cstring>)
strlen(name); // 获取长度(不含\0)
strcpy(dest, src); // 复制字符串(危险!容易溢出)
strcmp(a, b); // 比较字符串
char *****的三大痛点:
- 手动内存管理 :
char* result = new char[length + 1];之后必须delete[] result; - 容易越界:如果忘记分配足够的空间,程序会崩溃
- 不直观:拼接、查找等操作需要调用函数而非运算符
为什么PluginAPI还使用char*****:
因为PluginAPI定义在extern "C"块中(见1.5节),而extern "C"的目的是让C语言编译器也能理解这个接口。std::string是C++专有的,C语言不认识它。所以PluginAPI层面用const char*保证跨语言兼容。
项目实例 (plugins/weather/Weather.cpp:174-180):
cpp
// 插件在堆上分配char数组,返回给框架
std::string result = response.dump(); // C++ string
char* buffer = new char[result.length() + 1]; // 手动分配char数组
strcpy_s(buffer, result.length() + 1, result.c_str()); // 复制内容
return buffer; // 返回C风格字符串给框架
框架侧用完后再delete(src/main.cpp:234):
cpp
delete[] res_ptr; // 框架负责释放插件分配的char数组
1.4.2 C++的std::string
cpp
#include <string>
std::string s1 = "Hello"; // 从C字符串构造
std::string s2 = " World";
std::string s3 = s1 + s2; // 直接用+拼接!比strcat方便太多
size_t len = s3.length(); // 获取长度
bool same = (s1 == s2); // 直接用==比较!
s3 += "!"; // 追加
const char* c_str = s3.c_str(); // 需要C字符串时可转换
// std::string自动管理内存,离开作用域自动释放
1.5 内存模型:栈 vs 堆
理解栈和堆是理解C++内存管理、RAII、智能指针的基础。
plain
高地址
┌─────────────┐
│ 栈 │ ← 局部变量、函数参数。自动分配/释放,速度快,空间有限
│ ↓ │ 函数返回时自动销毁(FILO后进先出)
│ ... │
│ ↑ │
│ 堆 │ ← new/malloc分配的对象。手动释放,空间大,速度慢
├─────────────┤ 忘记释放 → 内存泄漏
│ 全局/静态区 │ ← 全局变量、static变量。程序启动分配,程序结束释放
├─────────────┤
│ 代码区 │ ← 程序指令
└─────────────┘
低地址
cpp
void example() {
// === 栈上分配 ===
int x = 5; // x在栈上,函数返回时自动销毁
std::string name = "hello"; // string对象在栈上(内部数据可能在堆上)
PluginTool tool; // 结构体在栈上
// === 堆上分配 ===
int* p = new int(5); // p在栈上(8字节指针),指向的int在堆上
delete p; // 必须手动释放!忘记就会内存泄漏
auto* tool = new PluginTool(); // 结构体在堆上
delete tool; // 必须手动释放
// === C++推荐方式 ===
auto tool2 = std::make_unique<PluginTool>(); // 智能指针,自动释放
// 离开作用域时,tool2自动delete
}
项目中的栈/堆使用:
cpp
// 栈上:PluginAPI.h 定义的纯数据结构
PluginTool methods[] = { ... }; // 数组在栈上(Weather.cpp:31)
// 堆上:用智能指针管理(main.cpp:40-41)
std::shared_ptr<vx::mcp::Server> server; // shared_ptr在栈上,指向堆上的Server
std::shared_ptr<vx::mcp::PluginsLoader> loader; // 同上
// 堆上:用unique_ptr管理(SseTransport.h:87)
std::unique_ptr<httplib::Server> server_; // 独占所有权的智能指针
为什么Server用shared_ptr而httplib::Server用unique_ptr?
因为Server被main和PluginsLoader(通过SetOnPluginsChanged回调中的lambda)共同持有引用,需要共享所有权。而httplib::Server只被SSE类持有,SSE类销毁时httplib::Server也应该被销毁------独占所有权场景。
2. 类与对象
2.1 class vs struct
在C++中,class和struct在技术上几乎完全相同,唯一的区别是默认访问权限:
cpp
struct MyStruct {
int x; // 默认 public
void foo() {} // 默认 public
};
class MyClass {
int x; // 默认 private!外部不可访问
void foo() {} // 默认 private!
public: // 需要显式声明
void bar() {}
};
习惯用法:
- struct用于纯数据聚合(POD,Plain Old Data)
- class用于有行为、有封装逻辑的类型
项目中的使用:
src/interface/PluginAPI.h:46-83:用typedef struct定义纯数据+函数指针的聚合------C兼容性的选择src/loader/PluginsLoader.h:58-98:struct PluginEntry------虽然是struct但用了C++特性(构造函数、=delete、析构函数),没有用class纯粹是因为所有成员确实需要public访问src/server/Server.h:47-123:class Server------有复杂的私有成员和公开接口,用class是标准选择
2.2 构造函数与析构函数
cpp
class Example {
public:
// 默认构造函数(无参数)
Example() {
// 对象创建时自动调用
}
// 带参数的构造函数
Example(int value) : data_(value) { // 初始化列表
// 对象创建时自动调用
}
// 析构函数(~类名,无参数,无返回值)
~Example() {
// 对象销毁时自动调用
// 用于释放资源、清理工作
}
private:
int data_ = 0;
};
初始化列表 vs 函数体赋值:
cpp
// 不好:先默认初始化,再赋值(两步操作)
Example(int value) {
data_ = value; // data_先被默认初始化为0,再被赋值为value
}
// 好:直接用初始化列表(一步操作,更高效)
Example(int value) : data_(value) { }
项目实际应用 (src/server/Server.cpp:34-59):
cpp
// Server构造函数:初始化functionMap------命令路由表
Server::Server() {
functionMap = {
{"initialize", [this](const json& req) { return this->InitializeCmd(req); }},
{"ping", [this](const json& req) { return this->PingCmd(req); }},
// ... 20多个命令映射 ...
};
}
这里展示了构造函数的经典用途:对象创建时完成初始化设置。Server一诞生就拥有完整的命令路由能力。
析构函数的RAII用途 (src/server/Server.cpp:61-63):
cpp
Server::~Server() {
Stop(); // 析构时自动停止所有线程、清理资源
}
2.3 拷贝构造与移动构造
C++有四种特殊的成员函数(C++11后)------合称"五法则"(Rule of Five):
| 函数 | 签名 | 用途 |
|---|---|---|
| 拷贝构造 | T(const T&) |
从另一个对象复制创建新对象 |
| 拷贝赋值 | T& operator=(const T&) |
将另一个对象的值复制给自己 |
| 移动构造 | T(T&&) |
从另一个对象"窃取"资源创建新对象 |
| 移动赋值 | T& operator=(T&&) |
从另一个对象"窃取"资源给自己 |
| 析构函数 | ~T() |
对象销毁时清理资源 |
cpp
class Buffer {
char* data_;
size_t size_;
public:
// 构造函数
Buffer(size_t size) : size_(size) {
data_ = new char[size];
}
// 拷贝构造------深拷贝
Buffer(const Buffer& other) : size_(other.size_) {
data_ = new char[size_];
std::memcpy(data_, other.data_, size_);
}
// 移动构造------窃取资源(浅拷贝+置空原对象)
Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 原对象不再拥有资源
other.size_ = 0;
}
// 析构函数
~Buffer() {
delete[] data_;
}
};
项目中"禁用拷贝"的模式 (src/loader/PluginsLoader.h:72-74):
cpp
struct PluginEntry {
// ...
PluginEntry(const PluginEntry&) = delete; // 禁止拷贝构造
PluginEntry& operator=(const PluginEntry&) = delete; // 禁止拷贝赋值
// 但允许移动构造/赋值(编译器自动生成)
};
为什么禁用拷贝? PluginEntry持有动态库句柄(handle)和插件实例指针(instance)。如果允许拷贝,会出现两个对象持有同一个handle------析构时一个对象先dlclose关闭了库,另一个再dlclose就会崩溃。这是经典的"独占资源"场景,只能移动不能拷贝。
类似地,src/server/Server.h:53-56:
cpp
Server(const Server&) = delete; // Server管理线程,不能拷贝
Server& operator=(const Server&) = delete;
Server(Server&&) = delete; // 连移动也禁用
Server& operator=(Server&&) = delete;
TSingleton也禁用了拷贝和移动(src/utils/TSingleton.h:36-39)。
2.4 public / private / protected
cpp
class Base {
public: // 任何人都能访问
void publicMethod() {}
protected: // 只有自己和派生类能访问
void protectedMethod() {}
private: // 只有自己能访问
int privateData_;
void privateMethod() {}
};
class Derived : public Base {
void test() {
publicMethod(); // OK
protectedMethod(); // OK - 派生类可以访问
// privateMethod(); // 错误!派生类也不能访问private
}
};
// 外部代码
Derived d;
d.publicMethod(); // OK
// d.protectedMethod(); // 错误
// d.privateData_; // 错误
项目中的实际应用 (src/loader/PluginsLoader.h:100-164):
cpp
class PluginsLoader {
public: // 公开接口------外部使用的API
bool LoadPlugins(const std::string& directory);
void UnloadPlugins();
std::vector<std::shared_ptr<PluginEntry>> GetPluginsSnapshot() const;
void StartWatching(const std::string& directory, ...);
// ...
private: // 私有实现细节------外部不需要知道
std::shared_ptr<PluginEntry> CreatePluginInstance(...);
std::string CopyToStaging(const std::string& originalPath);
void EnsureStagingDir(const std::string& pluginsDirectory);
// ...
private: // 私有数据成员
std::vector<std::shared_ptr<PluginEntry>> m_plugins; // m_前缀表示成员变量
mutable std::shared_mutex m_pluginsMutex;
std::thread m_watchThread;
// ...
};
封装的好处:外部代码(如main.cpp)只需知道LoadPlugins(path)和GetPluginsSnapshot(),完全不需要知道内部有staging目录、文件指纹缓存、mutex加锁等复杂逻辑。这些细节可以自由修改而不影响外部。
2.5 虚函数与纯虚函数(接口)
这是理解C++多态的核心概念。
回顾1.2节的PluginAPI------C语言通过函数指针实现"虚函数表"。C++的virtual关键字是这种模式的标准化和自动化。
cpp
// 普通成员函数:编译时确定调用哪个版本
class Animal {
public:
void speak() { std::cout << "Animal sound" << std::endl; }
};
// 虚函数:运行时根据实际对象类型确定调用哪个版本
class Animal {
public:
virtual void speak() { std::cout << "Animal sound" << std::endl; }
};
class Dog : public Animal {
public:
void speak() override { // override关键字:明确表示重写虚函数
std::cout << "Woof!" << std::endl;
}
};
// 使用
Animal* ptr = new Dog();
ptr->speak(); // 如果speak是virtual,输出"Woof!"
// 如果speak不是virtual,输出"Animal sound"(编译时绑定)
// 纯虚函数:没有实现的虚函数,使类成为"抽象类"
class ITransport { // "I"前缀惯例表示接口
public:
virtual bool Start() = 0; // = 0 表示纯虚函数,没有默认实现
virtual void Stop() = 0;
virtual ~ITransport() = default; // 虚析构函数(重要!)
};
// ITransport不能直接实例化:ITransport t; // 错误!
// 必须通过派生类实现所有纯虚函数后才能实例化
项目中的实际应用 (src/interface/ITransport.h:33-48):
这是整个项目架构设计的基石:
cpp
class ITransport {
public:
virtual bool Start() = 0; // 纯虚函数
virtual void Stop() = 0;
virtual bool IsRunning() = 0;
virtual std::pair<size_t, std::string> Read() = 0; // 同步读取
virtual void Write(const std::string& json_data) = 0; // 写入
virtual std::future<std::pair<size_t, std::string>> ReadAsync() = 0; // 异步读取
virtual std::future<void> WriteAsync(const std::string& json_data) = 0; // 异步写入
virtual std::string GetName() = 0;
virtual std::string GetVersion() = 0;
virtual int GetPort() = 0;
};
为什么这样设计? Server只依赖ITransport接口,不依赖具体传输实现。这使得可以用同一套Server代码支持三种完全不同的传输方式:
cpp
// main.cpp:120-126 ------ 根据命令行参数选择具体传输实现
if (use_sse_server->count() > 0) {
transport = std::make_shared<vx::transport::SSE>(); // HTTP SSE
} else if (use_httpstream_server->count() > 0) {
transport = std::make_shared<vx::transport::HttpStream>(); // HTTP Stream
} else {
transport = std::make_shared<vx::transport::Stdio>(); // 标准输入输出
}
三个传输类的声明(src/transport/StdioTransport.h:32):
cpp
class Stdio : public vx::ITransport { /* ... */ };
class SSE : public vx::ITransport { /* ... */ };
class HttpStream : public vx::ITransport { /* ... */ };
Server完全不关心底层是TCP、HTTP还是stdio,它只知道transport->Read()和transport->Write(data)。这就是**依赖倒置原则(DIP)**的经典实现。
虚析构函数为什么重要:
cpp
ITransport* ptr = new Stdio();
delete ptr; // 如果 ~ITransport() 不是 virtual,则只调用ITransport的析构函数
// Stdio的析构函数不会被调用 → 资源泄漏!
// 如果 ~ITransport() 是 virtual,则先调用 ~Stdio() 再调用 ~ITransport()
2.6 this指针
cpp
class Point {
int x, y;
public:
Point(int x, int y) {
this->x = x; // this->x 是成员变量,x是参数(参数名和成员名相同)
this->y = y;
}
Point& move(int dx, int dy) {
x += dx;
y += dy;
return *this; // 返回自身引用,支持链式调用
}
Point* getPointer() {
return this; // 返回指向自身的指针
}
};
// 链式调用
Point p(0, 0);
p.move(1, 2).move(3, 4); // 连续移动
this指针是每个非静态成员函数中隐含的参数------指向"调用这个函数的那个对象"。当你写plugin->GetName()时,在GetName函数内部,this就是plugin。
项目中this的典型用法 ------线程创建(src/server/Server.cpp:119):
cpp
writer_thread_ = std::thread(&Server::WriterLoop, this);
// ^^^^^^^^^^^^^^^^^ ^^^^
// 成员函数指针 this作为第一个参数传给线程
WriterLoop是Server的非静态成员函数,它需要一个this指针才能工作------因为它需要访问output_mutex_、notification_queue_等成员变量。创建线程时必须显式传递this。
2.7 静态成员函数
cpp
class MCPBuilder {
public:
// 静态函数:属于类本身,不属于任何实例
// 不需要对象就能调用
static json Response(json request) {
json response;
response["jsonrpc"] = "2.0";
response["id"] = request["id"];
return response;
}
static json Error(ErrorCode code, const std::string& id, const std::string& msg) {
return {{"jsonrpc", "2.0"}, {"error", {{"code", code}, {"message", msg}}}, {"id", id}};
}
};
// 调用静态函数------不需要创建MCPBuilder对象
json resp = MCPBuilder::Response(request);
json err = MCPBuilder::Error(code, id, msg);
项目中的实际应用 (src/utils/MCPBuilder.h:33-133):
整个MCPBuilder类只包含静态函数。它的角色是"工具函数集合"------没有状态,不需要实例化。这种模式有点像命名空间,但放在类里更内聚。
cpp
// 项目中的使用(Server.cpp:273)
return MCPBuilder::Error(MCPBuilder::InvalidRequest, request["id"], "Missing method");
// 项目中的使用(main.cpp:169)
MCPBuilder::NotificationToolsListChanged().dump().c_str()
3. RAII与智能指针
3.1 RAII原则详解
RAII = Resource Acquisition Is Initialization(资源获取即初始化)
这是C++最重要的惯用法。核心思想只有一句话:
将资源的生命周期绑定到对象的生命周期。
- 构造函数中获取资源(打开文件、申请内存、加锁、加载动态库)
- 析构函数中释放资源(关闭文件、释放内存、解锁、卸载动态库)
- 对象离开作用域时析构函数自动执行 → 资源自动释放 → 永不泄漏
cpp
// === 反例:传统C风格,容易出错 ===
void bad_example() {
FILE* f = fopen("data.txt", "r"); // 打开文件
char* buf = new char[1024]; // 申请内存
if (some_condition) {
return; // 提前返回!内存泄漏!文件未关闭!
}
if (error_occurs) {
throw std::runtime_error("oops"); // 抛出异常!内存泄漏!文件未关闭!
}
delete[] buf; // 手动释放
fclose(f); // 手动关闭
// 如果忘了写这两行 → 泄漏
// 如果有多个可能的退出路径 → 每个都要写释放代码
}
// === 正例:RAII风格 ===
void good_example() {
std::ifstream f("data.txt"); // 打开文件(构造函数)
std::vector<char> buf(1024); // 申请内存(构造函数)
if (some_condition) {
return; // 安全!f和buf的析构函数自动执行,文件关闭,内存释放
}
if (error_occurs) {
throw std::runtime_error("oops"); // 安全!栈展开时自动调用析构函数
}
// 正常结束也安全!析构函数自动执行
}
这就是为什么C++不需要finally块------RAII比finally更强大,不需要手动写清理代码。
3.2 项目中最精彩的RAII实例
PluginEntry的析构函数 (src/loader/PluginsLoader.h:76-97)是整个项目RAII设计的精华:
cpp
struct PluginEntry {
std::string path; // 插件原始路径
std::string stagingPath; // staging副本路径
LibraryHandle handle; // dlopen/LoadLibrary 返回的句柄
PluginAPI* instance; // 插件实例指针
// ~ 析构函数:6步自动清理,顺序至关重要! ~
~PluginEntry() {
if (instance) {
instance->Shutdown(); // 1. 通知插件关闭
delete instance->notifications; // 2. 释放通知系统
instance->notifications = nullptr;
destroyFunc(instance); // 3. 调用插件的DestroyPlugin函数
instance = nullptr;
}
if (handle) {
FreeLibrary(handle); // 4. Windows: 卸载动态库 (Linux: dlclose(handle))
handle = nullptr;
}
// 5. 清理staging副本文件
if (!stagingPath.empty() && stagingPath != path) {
std::error_code ec;
std::filesystem::remove(stagingPath, ec);
}
}
};
设计亮点:
- 6步清理,顺序不可颠倒(必须先Shutdown通知、再删除notifications、然后DestroyPlugin销毁实例、最后卸载动态库)
- 不需要手动调用任何清理函数------当最后一个shared_ptr释放时,这一切自动发生
- 即使程序异常退出,只要栈正常展开,清理就会执行
- staging文件的生命周期和插件实例完全绑定------插件卸载了,副本也自动删除
PluginsLoader::UnloadPlugins中的RAII (src/loader/PluginsLoader.cpp:238-246):
cpp
void PluginsLoader::UnloadPlugins() {
std::vector<std::shared_ptr<PluginEntry>> pluginsToRelease;
{
std::unique_lock lock(m_pluginsMutex); // RAII加锁
pluginsToRelease = std::move(m_plugins); // 移出所有插件
m_plugins.clear();
} // ← 离开作用域,unique_lock析构 → 自动解锁!
pluginsToRelease.clear(); // 释放shared_ptr → 逐个触发PluginEntry析构
}
注意unique_lock的使用------它在构造时加锁,离开作用域时自动解锁。即使在pluginsToRelease.clear()中抛出异常,锁也会被释放。这就是RAII的力量。
3.3 std::unique_ptr ------ 独占所有权
cpp
#include <memory>
// unique_ptr:独占所有权,不能被复制,只能被移动
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // 错误!不能复制
std::unique_ptr<int> p2 = std::move(p1); // OK:转移所有权
// p1现在是nullptr,p2拥有int对象
// 离开作用域时自动delete
void function() {
auto ptr = std::make_unique<LargeObject>(/* 大量数据 */);
// ... 使用ptr ...
} // ptr析构 → delete LargeObject → 内存自动释放
项目中的实际应用 (src/utils/TSingleton.h:54):
cpp
template <typename T>
class TSingleton {
private:
static std::unique_ptr<T> instance; // 单例实例的唯一所有者
// ...
};
// 静态成员定义
template <typename T>
std::unique_ptr<T> TSingleton<T>::instance = nullptr;
使用unique_ptr存储单例实例意味着:程序结束时,unique_ptr自动调用析构函数释放单例。不需要手动管理。
另外一个例子(src/transport/SseTransport.h:87):
cpp
std::unique_ptr<httplib::Server> server_;
SSE传输对象是httplib::Server的唯一拥有者------当SSE对象销毁时,HTTP服务器自动停止。
3.4 std::shared_ptr ------ 共享所有权(引用计数)
这是GetPluginsSnapshot()工作机制的核心!
cpp
// shared_ptr:通过引用计数实现共享所有权
std::shared_ptr<int> p1 = std::make_shared<int>(42); // 引用计数 = 1
{
std::shared_ptr<int> p2 = p1; // 引用计数 = 2
std::shared_ptr<int> p3 = p1; // 引用计数 = 3
} // p2, p3 析构 → 引用计数 = 1
// p1 析构 → 引用计数 = 0 → delete int对象
为什么需要引用计数? 考虑这个场景:
- 线程A正在遍历插件列表处理请求(持有插件列表的快照)
- 同时,文件监控线程B发现插件文件更新了,要替换插件
如果处理完请求后发现插件被销毁了 → 访问已释放的内存 → 程序崩溃!
shared_ptr解决了这个问题:只要还有请求持有插件的shared_ptr,插件就不会被销毁。引用计数降到0时才真正析构。
项目实现 (src/loader/PluginsLoader.cpp:248-251):
cpp
std::vector<std::shared_ptr<PluginEntry>> PluginsLoader::GetPluginsSnapshot() const {
std::shared_lock lock(m_pluginsMutex); // 读锁(允许多个读并发)
return m_plugins; // 返回副本,每个shared_ptr的引用计数+1
}
当调用者获得快照后,即使PluginsLoader内部替换了m_plugins中的元素,调用者持有的PluginEntry仍然有效------因为它的引用计数不为0。
cpp
// main.cpp:199 ------ 使用快照
auto plugins = loader->GetPluginsSnapshot(); // 获取快照,引用计数+1
for (const auto& plugin : plugins) { // 安全遍历
plugin->instance->GetName(); // 即使热加载替换,这里仍然安全
}
// 离开作用域,引用计数-1
Server::Stop中对transport的处理 (src/server/Server.cpp:226-231):
cpp
if (transport_) {
transport_->Stop();
transport_.reset(); // shared_ptr::reset() → 引用计数-1
}
3.5 std::weak_ptr ------ 解决循环引用
cpp
// 循环引用问题示例
struct B; // 前置声明
struct A {
std::shared_ptr<B> ptr_to_b; // A持有B的shared_ptr
~A() { std::cout << "A destroyed" << std::endl; }
};
struct B {
std::shared_ptr<A> ptr_to_a; // B持有A的shared_ptr
~B() { std::cout << "B destroyed" << std::endl; }
};
// 创建循环引用
auto a = std::make_shared<A>(); // A引用计数=1
auto b = std::make_shared<B>(); // B引用计数=1
a->ptr_to_b = b; // B引用计数=2
b->ptr_to_a = a; // A引用计数=2
// a和b离开作用域 → 各自引用计数-1 → 都变成1 → 永远不为0 → 内存泄漏!
// 两个析构函数都不会被调用!
// 解决方案:用weak_ptr打破循环
struct B {
std::weak_ptr<A> ptr_to_a; // weak_ptr不增加引用计数
// ...
};
// 现在B引用计数可以降到0 → B析构 → A的引用计数降到0 → A析构
本项目目前没有明显的循环引用场景(插件和Server之间通过回调通信,没有相互持有shared_ptr),但理解weak_ptr对阅读大型C++项目至关重要。
3.6 shared_mutex ------ 读写锁的RAII包装
这是项目中多次出现的重要RAII模式(src/loader/PluginsLoader.h:148):
cpp
mutable std::shared_mutex m_pluginsMutex;
两种锁模式:
cpp
// === 读锁(共享锁):多个线程可以同时持有 ===
std::shared_lock lock(m_pluginsMutex); // RAII获取读锁
// ... 读取 m_plugins ...
// 离开作用域,自动释放读锁
// === 写锁(独占锁):同时只有一个线程持有 ===
std::unique_lock lock(m_pluginsMutex); // RAII获取写锁
// ... 修改 m_plugins ...
// 离开作用域,自动释放写锁
项目中的使用模式 (src/loader/PluginsLoader.cpp):
| 操作 | 锁类型 | 位置 |
|---|---|---|
| GetPluginsSnapshot() | shared_lock (读) | 行249 |
| LoadPlugins() 添加插件 | unique_lock (写) | 行225 |
| UnloadPlugins() 清空列表 | unique_lock (写) | 行241 |
| ScanForChanges() 收集变更 | shared_lock (读) | 行336 |
| ScanForChanges() 执行变更 | unique_lock (写) | 行424 |
这种模式保证了:多个请求可以同时读取插件列表(高并发),但修改插件列表是互斥的。
4. 模板与泛型编程
4.1 函数模板
cpp
// 不用模板:需要为每种类型写一个函数
int max_int(int a, int b) { return a > b ? a : b; }
double max_double(double a, double b) { return a > b ? a : b; }
// 用模板:一个函数适用于所有类型
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 使用(编译器自动推导类型)
int r1 = max(3, 5); // T = int
double r2 = max(3.14, 2.71); // T = double
std::string r3 = max(std::string("abc"), std::string("xyz")); // T = std::string
模板不是魔法------编译器在编译时为你使用的每种类型"自动生成"一个具体版本。max(3, 5)会生成一个int max(int, int)函数。这叫做模板实例化。
4.2 类模板
cpp
// 一个通用的容器类
template <typename T>
class MyVector {
T* data_;
size_t size_;
public:
void push_back(const T& value) { /* ... */ }
T& at(size_t index) { return data_[index]; }
size_t size() const { return size_; }
};
// 使用
MyVector<int> intVec; // T = int,存储整数
MyVector<std::string> strVec; // T = std::string,存储字符串
4.3 项目中的TSingleton详解
这是模板在实际项目中应用的绝佳案例 (src/utils/TSingleton.h:31-63):
cpp
template <typename T>
class TSingleton
{
public:
// ===== 禁止拷贝和移动 =====
TSingleton(const TSingleton&) = delete;
TSingleton(TSingleton&&) = delete;
TSingleton& operator=(const TSingleton&) = delete;
TSingleton& operator=(TSingleton&&) = delete;
// ===== 获取单例实例(静态方法) =====
static T& GetInstance()
{
std::call_once(initFlag, []() { // 保证只执行一次,线程安全
instance.reset(new T()); // 创建唯一的T实例
});
return *instance; // 返回引用
}
protected:
TSingleton() = default; // 构造函数protected:只能被派生
virtual ~TSingleton() = default; // 虚析构函数
private:
static std::unique_ptr<T> instance; // 单例实例的唯一所有者
static std::once_flag initFlag; // 配合call_once的线程安全标志
};
// 静态成员必须在类外定义
template <typename T>
std::unique_ptr<T> TSingleton<T>::instance = nullptr;
template <typename T>
std::once_flag TSingleton<T>::initFlag;
逐行深度解析:
template <typename T>:这是一个类模板。T是占位符,使用时会被具体类型替换。例如TSingleton<Server>中T就是Server。= delete四个重载版本:禁止了拷贝构造、移动构造、拷贝赋值、移动赋值的所有可能性。单例模式的核心约束------不能有第二个实例。static T& GetInstance():静态成员函数,不需要对象就能调用。返回引用而非指针,表示"一定存在,不能为空"。std::call_once(initFlag, ...):- 保证lambda中的初始化代码在多线程环境下只执行一次
- 比手动写"双重检查锁定"模式简单且正确
- 第一个线程执行初始化,其他线程等待初始化完成
instance.reset(new T()):用new创建T的实例,交给unique_ptr管理。reset表示"放弃旧指针(如果有),持有新指针"。protected** 构造函数**:外部不能直接创建TSingleton对象,只能通过继承使用。这强制了正确的使用方式:class MyClass : public TSingleton<MyClass> { ... };virtual ~TSingleton() = default:虚析构函数确保通过基类指针删除派生类对象时,派生类的析构函数能被正确调用。
线程安全性分析:
cpp
// 错误的单例实现(非线程安全)
static T& GetInstance() {
static T instance; // C++11保证了局部static变量的线程安全初始化
return instance; // 但这是C++11的魔法,不是所有编译器都正确实现
}
// 本项目使用 std::call_once ------ 标准、明确、可移植
// call_once比局部static更可靠,因为在DLL/动态库加载场景下
// 局部static的初始化行为可能不一致
4.4 模板的"代码膨胀"问题
cpp
// 每个不同的T都会生成一份独立的代码
TSingleton<Server>; // 生成一份 GetInstance 代码
TSingleton<PluginsLoader>; // 生成另一份 GetInstance 代码
TSingleton<Logger>; // 又生成一份
// 对于TSingleton这种轻量级模板来说完全可接受,
// 但对于大型模板(如std::vector<T>),代码膨胀是需要考虑的
5. Lambda表达式与std::function
5.1 Lambda语法
cpp
// Lambda 完整语法:
// [捕获列表](参数列表) -> 返回类型 { 函数体 }
// 最简形式
auto hello = []() { std::cout << "Hello!" << std::endl; };
hello(); // 调用
// 带参数
auto add = [](int a, int b) { return a + b; };
int result = add(3, 5); // result = 8
// 带返回类型推断(通常省略 -> 返回类型)
auto square = [](double x) { return x * x; }; // 返回double,自动推断
// 显式指定返回类型(当有多种返回路径时需要)
auto safe_div = [](int a, int b) -> std::optional<int> {
if (b == 0) return std::nullopt;
return a / b;
};
5.2 捕获列表
捕获列表是Lambda最强大的特性------让Lambda能访问外部变量。
cpp
int x = 10, y = 20;
std::string name = "Alice";
// [=] 按值捕获所有外部变量(在Lambda内部是副本)
auto f1 = [=]() { return x + y; }; // 捕获了x和y的副本
// [&] 按引用捕获所有外部变量(在Lambda内部是引用)
auto f2 = [&]() { x = 100; }; // 修改会影响外部的x
// [this] 按引用捕获当前对象(在成员函数内使用)
// [x, &y] 按值捕获x,按引用捕获y
// [=, &name] 除name按引用外,其余按值捕获
项目中最经典的Lambda用法 (src/server/Server.cpp:35-58):
cpp
Server::Server() {
functionMap = {
{"initialize", [this](const json& req) { return this->InitializeCmd(req); }},
{"ping", [this](const json& req) { return this->PingCmd(req); }},
{"tools/list", [this](const json& req) { return this->ToolsListCmd(req); }},
{"tools/call", [this](const json& req) { return this->ToolsCallCmd(req); }},
// ... 共20多个映射 ...
};
}
逐行解读:
functionMap的类型是std::unordered_map<std::string, std::function<json(const json&)>>- 每个value是一个Lambda:
[this](const json& req) { return this->InitializeCmd(req); } [this]捕获this指针 → Lambda内部可以调用this->InitializeCmd(req)等成员函数- Lambda的类型被擦除为
std::function<json(const json&)>
为什么这样设计? 这是一个典型的"命令模式"实现。收到JSON消息后,根据method字段查表找到对应的处理函数,然后调用。如果用if-else链或switch-case,每增加一个命令都需要修改代码;用map+Lambda,增加命令只需在构造函数的初始化列表里加一行。
5.3 std::function ------ 类型擦除
cpp
#include <functional>
// std::function 可以存储任何"可调用对象":函数指针、Lambda、函数对象
std::function<int(int, int)> func;
func = [](int a, int b) { return a + b; }; // 存储Lambda
func(3, 5); // 8
func = std::multiplies<int>(); // 存储函数对象
func(3, 5); // 15
int add(int a, int b) { return a + b; }
func = &add; // 存储普通函数指针
func(3, 5); // 8
项目中的应用 (src/loader/PluginsLoader.h:102-105):
cpp
// 定义回调类型
using OnPluginLoaded = std::function<void(PluginEntry&)>;
using OnPluginsChanged = std::function<void(bool, bool, bool)>;
// 在PluginsLoader中存储回调
OnPluginLoaded m_onPluginLoaded;
OnPluginsChanged m_onPluginsChanged;
// 在main.cpp中设置回调(注入依赖)
loader->SetOnPluginLoaded([](vx::mcp::PluginEntry& plugin) {
plugin.instance->notifications = new NotificationSystem();
plugin.instance->notifications->SendToClient = ClientNotificationCallbackImpl;
});
这里的Lambda捕获了notificationState(通过引用或值?实际上是隐含捕获了全局的ClientNotificationCallbackImpl函数指针),将Server的通知能力注入到每个新加载的插件中。这是依赖注入的C++实现。
Server::OverrideCallback (src/server/Server.cpp:296-301)也是同样的模式:
cpp
bool Server::OverrideCallback(const std::string &method,
std::function<json(const json &)> function) {
if (functionMap.find(method) != functionMap.end()) {
functionMap[method] = std::move(function); // 移动而非拷贝
return true;
}
return false;
}
5.4 Lambda用于异步编程
项目中的异步Reader线程 (src/server/Server.cpp:177-215):
cpp
reader_running_ = true;
reader_thread_ = std::thread([this]() { // Lambda作为线程入口
LOG(INFO) << "Async Reader thread started." << std::endl;
while (reader_running_ && !isStopping_) {
try {
auto future = transport_->ReadAsync(); // 发起异步读取
auto [length, json_string] = future.get(); // 等待结果
if (isStopping_ || (length == 0 && json_string.empty())) {
break;
}
if (!json_string.empty()) {
json request = json::parse(json_string);
// ... 处理请求 ...
}
} catch (const std::exception &e) {
isStopping_ = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
Lambda [this]() { ... } 被传给std::thread,在新线程中执行。[this]捕获了Server对象,使新线程能够访问所有Server的成员变量。
6. C++17/20核心新特性
6.1 auto类型推导
cpp
// 让编译器自动推导变量类型
auto i = 42; // int
auto d = 3.14; // double
auto s = std::string("hello"); // std::string
auto vec = std::vector<int>{1,2,3}; // std::vector<int>
// 项目中的使用
auto entry = std::make_shared<PluginEntry>(); // std::shared_ptr<PluginEntry>
auto now = std::chrono::steady_clock::now(); // std::chrono::...::time_point
auto canonicalFile = std::filesystem::weakly_canonical(filePath, ec);
什么时候用auto,什么时候不?
- 当类型显而易见时(
auto p = std::make_unique<T>())→ 用auto - 当类型不明确时(
auto result = someFunction())→ 写完整类型更好 - 迭代器、Lambda、模板嵌套类型 → 用auto大幅简化代码
6.2 结构化绑定(Structured Bindings,C++17)
cpp
// 传统写法
std::pair<int, std::string> result = getResult();
int code = result.first;
std::string msg = result.second;
// C++17 结构化绑定 ------ 一行搞定
auto [code, msg] = getResult();
// 项目中的实际应用(Server.cpp:128)
auto [length, json_string] = transport->Read();
// ^^^^^^ ^^^^^^^^^^^^
// 自动拆解 std::pair<size_t, std::string>
// 另一个例子(Server.cpp:182)
auto [length, json_string] = future.get();
结构化绑定也适用于自定义结构体:
cpp
struct Point { int x; int y; };
Point p{10, 20};
auto [x, y] = p; // x=10, y=20
// map遍历中的使用(PluginsLoader.cpp:357)
for (const auto& [path, info] : currentFiles) {
// path是const std::string&, info是const FileInfo&
}
6.3 std::optional(C++17)
cpp
// 表示"可能有值,也可能没有"的值
std::optional<int> divide(int a, int b) {
if (b == 0) return std::nullopt; // 表示"无值"
return a / b; // 表示"有值"
}
// 使用
auto result = divide(10, 2);
if (result.has_value()) { // 或者 if (result)
std::cout << *result; // 解引用获取值,或者 result.value()
}
int safe = result.value_or(-1); // 有值返回值,无值返回默认值-1
本项目目前没有直接使用std::optional(接口设计中使用char*和nullptr表示"无结果"是C兼容性的选择),但理解它对阅读现代C++代码很重要。
6.4 std::variant(C++17)
cpp
// 类型安全的union------可以存储多种类型之一
std::variant<int, double, std::string> v;
v = 42; // 存储int
v = 3.14; // 存储double(替换之前的int)
v = std::string("hello"); // 存储string
// 访问variant的值
std::visit([](auto&& arg) {
std::cout << arg << std::endl;
}, v);
// 或使用std::get
if (std::holds_alternative<int>(v)) {
int val = std::get<int>(v);
}
6.5 if constexpr(C++17)
cpp
// 编译时条件判断------无效分支不会被编译
template <typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
// 如果T是整数类型,编译这段代码
std::cout << "Integer: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
// 如果T是浮点类型,编译这段代码
std::cout << "Float: " << value << std::endl;
} else {
// 其他类型
std::cout << "Other type" << std::endl;
}
}
与普通if的区别:普通if在运行时判断,两个分支都会被编译;if constexpr在编译时判断,只有匹配的分支被编译。这避免了"对int类型调用浮点函数"等编译错误。
6.6 std::jthread(C++20)
cpp
// std::thread 的问题:忘记join/detach → 程序terminate
// std::jthread 的优势:析构函数自动join,并且支持停止令牌
#include <thread>
// 传统 std::thread
std::thread t([&]() {
while (running_) { // 需要额外的atomic<bool>来控制停止
// 工作...
}
});
t.join(); // 必须显式调用,否则 crash
// C++20 std::jthread
std::jthread jt([](std::stop_token stoken) {
while (!stoken.stop_requested()) { // 用停止令牌代替自定义flag
// 工作...
if (stoken.stop_requested()) break;
}
});
// 析构函数自动请求停止并join,不需要手动管理
本项目为什么没用jthread? 项目使用传统的std::thread + std::atomic<bool>模式。这在C++20项目中完全有效,只是jthread是更现代的替代方案。如果想升级,Server.cpp:119的writer_thread_和Server.cpp:177的reader_thread_都是jthread的理想候选。
6.7 std::span(C++20)
cpp
#include <span>
// span 是一个"视图"------不拥有数据,只是指向一段连续内存的窗口
void process_data(std::span<int> data) { // 替代 int* + size_t
for (int val : data) { // 支持范围for
std::cout << val << std::endl;
}
std::cout << "Size: " << data.size() << std::endl;
}
// 使用
int arr[] = {1, 2, 3, 4, 5};
process_data(arr); // 数组自动转为span
std::vector<int> vec = {6, 7, 8};
process_data(vec); // vector也自动转为span
process_data({arr, 3}); // 前3个元素的span
7. Move语义
7.1 左值、右值、将亡值
理解移动语义的第一步是分清值的类别:
plain
表达式
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
(左值) (将亡值) (纯右值)
通俗理解(不需要死记硬背术语):
cpp
int x = 42; // x 是左值------有名字、有地址、生命周期长
int y = x + 1; // x + 1 是右值------临时结果,没名字,即将消失
int z = std::move(x); // std::move(x) 是将亡值------"我马上要死了,资源拿去吧"
bool is_lvalue = std::is_lvalue_reference_v<decltype((x))>; // true
bool is_rvalue = std::is_rvalue_reference_v<decltype((x + 1))>; // true
bool is_rvalue2 = std::is_rvalue_reference_v<decltype((std::move(x)))>; // true
口诀:
- 左值 :能取地址的(
&x合法)、有名字的 - 右值 :不能取地址的(
&42非法)、临时的 - 将亡值 :被
std::move标记为"即将销毁"的
7.2 为什么需要移动语义
cpp
// 场景:函数返回一个大字符串
std::string createBigString() {
std::string result(1000000, 'x'); // 100万个'x'
return result; // C++11之前:返回时拷贝整个字符串 → 复制100万字节!
// C++11之后:移动语义 → 只复制指针、大小、容量(24字节)
}
// 场景:把对象放进容器
std::vector<std::string> vec;
std::string s(1000000, 'x');
vec.push_back(s); // 拷贝:s的内容不变,vec里有一份副本
vec.push_back(std::move(s)); // 移动:s变成空字符串,资源转移到vec中
// s现在不应该再被使用(处于"合法但未指定"的状态)
移动的本质:不复制资源,而是"窃取"资源。
cpp
// std::string的简化移动构造
string(string&& other) noexcept {
data_ = other.data_; // 窃取指针(只复制8字节!)
size_ = other.size_;
capacity_ = other.capacity_;
other.data_ = nullptr; // 原对象不再拥有数据
other.size_ = 0;
other.capacity_ = 0;
}
// 只复制了3个指针/整数(~24字节),而不是100万个字符
7.3 std::move
cpp
// std::move 本身不移动任何东西!
// 它只是一个类型转换:把左值强制转换为右值引用
// 真正的移动发生在移动构造函数或移动赋值运算符中
std::string s1 = "hello";
std::string s2 = std::move(s1); // std::move(s1) 将s1转换为 string&&
// 触发 string 的移动构造函数
// s1的内容被"偷走",s1现在为空
项目中的std::move使用 (src/loader/PluginsLoader.cpp:211):
cpp
newEntries.push_back(std::move(entry));
这里entry是std::shared_ptr<PluginEntry>,std::move避免了引用计数的原子操作(临时增减),直接将指针所有权转移给vector。
Server::OverrideCallback中的std::move (src/server/Server.cpp:298):
cpp
functionMap[method] = std::move(function);
避免拷贝std::function对象(可能包含大量捕获信息),直接转移所有权。
SetOnPluginLoaded中的std::move (src/loader/PluginsLoader.cpp:254):
cpp
m_onPluginLoaded = std::move(callback);
7.4 std::forward ------ 完美转发
cpp
// 问题:如何写一个包装函数,将参数原封不动地转发给另一个函数?
// 如果直接传递,会丢失右值信息
// 不用 forward ------ 右值变成了左值
template <typename T>
void wrapper_bad(T&& arg) {
callee(arg); // arg现在有名字 → 是左值 → 总是触发拷贝
}
// 用 forward ------ 保持原始值类别
template <typename T>
void wrapper_good(T&& arg) {
callee(std::forward<T>(arg)); // 左值保持左值,右值保持右值
}
什么时候用move vs forward:
std::move(x):你确定不再需要x,想转移它的资源std::forward<T>(x):你不知道x是左值还是右值,你想保持它本来的属性
7.5 移动构造/移动赋值与 noexcept
cpp
class MyClass {
public:
// 移动构造应声明为 noexcept
MyClass(MyClass&& other) noexcept : data_(other.data_) {
other.data_ = nullptr;
}
// 移动赋值也应声明为 noexcept
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data_; // 释放旧资源
data_ = other.data_; // 窃取新资源
other.data_ = nullptr;
}
return *this;
}
private:
int* data_;
};
为什么移动操作要标记noexcept?
因为标准库容器(如std::vector)在扩容时会检查移动构造是否为noexcept:
- 如果noexcept → 使用移动(高效)
- 如果不是noexcept → 使用拷贝(安全但慢)
这是因为如果移动过程中抛出异常,原对象的状态已损坏,数据无法恢复。所以容器宁愿慢一点拷贝,也不冒险移动。
项目中的示例 (src/loader/PluginsLoader.h:72-74):
cpp
PluginEntry(const PluginEntry&) = delete; // 禁止拷贝
PluginEntry& operator=(const PluginEntry&) = delete; // 禁止拷贝
// 编译器自动生成的移动构造/赋值是 noexcept 的(因为成员都是可移动的)
8. 其他关键知识点
8.1 enum class(C++11)
cpp
// === 传统C枚举:值污染全局命名空间 ===
enum Color { RED, GREEN, BLUE };
enum TrafficLight { RED, YELLOW, GREEN }; // 编译错误!RED已经定义过了
// === C++11 enum class:强类型、有作用域 ===
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green }; // OK!作用域隔离
Color c = Color::Red; // 必须用作用域限定符
TrafficLight t = TrafficLight::Red; // 不会冲突
// int x = Color::Red; // 错误!不能隐式转换为int
int x = static_cast<int>(Color::Red); // 必须显式转换
项目中的实际应用 (src/loader/PluginsLoader.h:123-127):
cpp
enum class LoadResult {
kSuccess, // 加载成功
kLoadFailed, // 插件本身加载/初始化失败
kSourceChangedDuringCopy // 复制期间源文件被覆盖,下轮重试
};
使用enum class意味着这些值不会泄漏到vx::mcp命名空间之外,也不会和其他枚举值冲突(比如任何库都可能定义kSuccess)。
另一个例子 (src/interface/PluginAPI.h:40-44)使用的是传统C枚举(因为PluginAPI在extern "C"块内,需要C兼容性):
cpp
typedef enum {
PLUGIN_TYPE_TOOLS = 0,
PLUGIN_TYPE_PROMPTS = 1,
PLUGIN_TYPE_RESOURCES = 2
} PluginType;
8.2 namespace
cpp
// 命名空间:防止名字冲突
namespace my_project {
class Server { /* ... */ };
void initialize() { /* ... */ }
}
namespace third_party {
class Server { /* ... */ }; // 不会和my_project::Server冲突
}
// 使用
my_project::Server s;
my_project::initialize();
// using声明
using my_project::Server;
Server s; // 不需要前缀了
// using namespace(不要在头文件中使用!)
using namespace my_project; // 污染当前作用域,不推荐
项目中的命名空间层次 (CMakeLists.txt:23-30及各头文件):
plain
vx (src/interface/ITransport.h:31 namespace vx)
├── mcp (src/server/Server.h:39 namespace vx::mcp)
├── transport (src/transport/StdioTransport.h:30 namespace vx::transport)
└── utils (src/utils/SessionBuilder.h:33 namespace vx::utils)
嵌套命名空间的声明(C++17前):
cpp
namespace vx {
namespace mcp {
class Server { /* ... */ };
}
}
C++17简化写法:
cpp
namespace vx::mcp {
class Server { /* ... */ };
}
8.3 const与constexpr
cpp
// === const:运行时不可修改 ===
const int MAX_SIZE = 100; // 常量,不能重新赋值
const std::string name = "Alice";
void foo(const std::string& s) { // const引用:承诺不修改参数
// s += "!"; // 错误!const引用不能修改
std::cout << s.length(); // 可以读取
}
class MyClass {
void bar() const { // const成员函数:承诺不修改任何成员变量
// data_ = 5; // 错误!const成员函数不能修改成员
return data_; // 可以读取
}
};
// === constexpr:编译时求值 ===
constexpr int square(int x) { return x * x; }
int arr[square(5)]; // 正确!square(5)在编译时就算出=25,数组大小是编译时常量
constexpr int MAX = 100; // 编译时常量
const int RUNTIME = read_input(); // 运行时确定(const不一定是编译时常量)
项目中的应用 (src/server/Server.h:37):
cpp
#define MAX_PARSER_ERRORS 50 // 宏(C风格),C++中更推荐 constexpr
现代C++写法:
cpp
constexpr int kMaxParserErrors = 50; // 类型安全,有作用域
const成员函数 (src/loader/PluginsLoader.h:113):
cpp
std::vector<std::shared_ptr<PluginEntry>> GetPluginsSnapshot() const;
// ^^^^^
// 这个const承诺:调用这个函数不会修改PluginsLoader对象的状态
// 但这不阻止返回的shared_ptr引用计数变化------
// 所以mutex声明为 mutable(见下节)
8.4 = delete / = default
cpp
class Example {
public:
// = default:让编译器生成默认实现
Example() = default; // 默认构造函数
~Example() = default; // 默认析构函数
Example(const Example&) = default; // 默认拷贝构造
Example& operator=(const Example&) = default; // 默认拷贝赋值
// = delete:禁止使用
Example(Example&&) = delete; // 禁止移动构造
Example& operator=(Example&&) = delete; // 禁止移动赋值
void dangerous_operation() = delete; // 甚至可以删除普通函数!
};
// 使用场景1:单例模式禁止拷贝
// TSingleton.h:36-39
// 使用场景2:资源管理类禁止拷贝但允许移动
// PluginEntry:72-74
// 使用场景3:Server类完全禁止拷贝和移动(管理线程)
// Server.h:53-56
8.5 mutable
cpp
class Cache {
public:
int getValue(int key) const { // const成员函数
if (!cached_) {
// 但我们需要修改cache_(不改变对象的"逻辑状态")
cache_ = expensive_computation(); // 错误!const成员函数不能修改成员
}
return cache_;
}
private:
int cache_;
bool cached_;
};
// 解决方案:mutable
class Cache {
public:
int getValue(int key) const {
if (!cached_) {
cache_ = expensive_computation(); // OK!mutable成员可以在const函数中修改
cached_ = true;
}
return cache_;
}
private:
mutable int cache_; // mutable:即使在const成员函数中也能修改
mutable bool cached_;
};
项目中的应用 (src/loader/PluginsLoader.h:148):
cpp
mutable std::shared_mutex m_pluginsMutex;
为什么mutex需要mutable?因为GetPluginsSnapshot() const声明为const成员函数(承诺不修改逻辑状态),但它需要加读锁------这改变了mutex的内部状态。mutable允许这种"不影响对象逻辑状态的物理修改"。
8.6 extern "C" ------ 为什么PluginAPI需要它
这是连接C和C++世界的关键语法 (src/interface/PluginAPI.h:34-36,88-90):
cpp
#ifdef __cplusplus
extern "C" {
#endif
// ... 这里的所有声明都使用C语言链接规则 ...
#ifdef __cplusplus
}
#endif
为什么需要? C++有一个"C语言没有"的特性:函数重载和命名空间带来的名字改编(Name Mangling)。
cpp
// C++编译后的符号名(简化示意)
void foo(int x); // 编译后 → _Z3fooi
void foo(double x); // 编译后 → _Z3food (名字被"改编"以区分重载)
void foo(int, int); // 编译后 → _Z3fooii
// C编译后的符号名
void foo(int x); // 编译后 → foo (就是函数名本身,没有改编)
问题来了:动态库中的CreatePlugin函数如果是C++编译的,会变成类似_Z12CreatePluginv的名字。而使用dlopen+dlsym("CreatePlugin")按字符串查找符号时,找不到这个名字!
extern "C"告诉C++编译器:用C语言的规则(不改编名字)编译这段代码。
cpp
// plugins/weather/Weather.cpp:212-218
extern "C" PLUGIN_API PluginAPI* CreatePlugin() {
return &plugin;
}
extern "C" PLUGIN_API void DestroyPlugin(PluginAPI*) {
// Nothing to clean up
}
编译后,这些函数的名字在动态库中就是CreatePlugin和DestroyPlugin(而不是改编后的名字),所以框架可以通过dlsym(handle, "CreatePlugin")找到它们。
PLUGIN_API宏的作用 (PluginAPI.h:28-32):
cpp
#ifdef _WIN32
#define PLUGIN_API __declspec(dllexport) // Windows: 导出符号
#else
#define PLUGIN_API __attribute__((visibility("default"))) // Linux/macOS: 默认可见性
#endif
确保这些函数在动态库中"可见"(可被外部代码调用)。
8.7 多线程基础:项目中使用的同步原语
项目使用了丰富的多线程机制,这里汇总介绍:
std::atomic ------ 无锁的原子操作
cpp
// atomic:保证多线程读写的原子性,不需要mutex
std::atomic<bool> isStopping_{false}; // Server.h:106
std::atomic<bool> writer_running_{false};
// 所有操作都是原子的
isStopping_.store(true); // 原子写入
if (isStopping_.load()) { ... } // 原子读取
bool expected = false;
bool exchanged = isStopping_.exchange(true); // 原子交换(读+写一步完成)
项目中的信号安全设计(src/server/Server.cpp:245-247):
cpp
void Server::RequestStop() {
isStopping_.store(true); // 只有原子操作,可在信号处理函数中安全调用!
}
std::mutex / std::lock_guard / std::unique_lock
cpp
std::mutex mtx; // 互斥锁
// lock_guard: 最简RAII锁(不能手动解锁)
void simple() {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁
// ... 临界区 ...
} // 析构时解锁
// unique_lock: 更灵活的RAII锁(可以手动解锁、延迟加锁)
void flexible() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不立即加锁
// ... 准备工作 ...
lock.lock(); // 手动加锁
// ... 临界区 ...
lock.unlock(); // 手动解锁
// ... 更多工作 ...
}
项目中使用lock_guard保护通知队列(src/server/Server.cpp:257-259):
cpp
std::lock_guard<std::mutex> lock(output_mutex_);
notification_queue_.emplace(notification);
std::condition_variable ------ 线程间通信
cpp
// 生产者-消费者模式
std::mutex mtx;
std::condition_variable cv;
std::queue<std::string> queue;
// 生产者
void producer() {
std::lock_guard lock(mtx);
queue.push("data");
cv.notify_one(); // 唤醒一个等待的消费者
}
// 消费者
void consumer() {
std::unique_lock lock(mtx);
cv.wait(lock, [&]{ return !queue.empty(); }); // 等待直到队列非空
// wait内部:先解锁并阻塞,被notify唤醒后重新加锁,检查条件
// 条件为false → 重新解锁阻塞
// 条件为true → 持有锁继续执行
std::string data = queue.front();
queue.pop();
}
项目中Server的WriterLoop使用了这个模式(src/server/Server.cpp:67-104)。
std::shared_mutex ------ 读写锁(C++17)
已在3.6节详细介绍。参见PluginsLoader.cpp中shared_lock(读)和unique_lock(写)的使用。