跳转至内容

Blogs | 博客

Blog posts from individual members | 创建个人博客版块

36 主题 74 帖子

子版块


  • Sunrisepeak的博客

    7 主题
    16 帖子
    SPeakS
    文章导读 一、类模板与模板特化(全特化与偏特化) 二、类模板与其模板特化的应用 三、模板特化存在性问题 一、类模板与模板特化(全特化与偏特化)

    从接受类型的角度
    类模板:全集R
    模板偏特化(部分特化):为全集的一个子集A
    模板全特化:为R中的一个"点",或者说为R中的一个元素
    匹配规则:越特化匹配优先级越高(见下面例子)

    98a9da6a-01af-4fc4-83ad-91b7ca549699-image.png

    1.类模板

    可以接受任意类型

    // R template<typename T> class A {}; // 类模板是能接受任意类型,A后面不需要(不能)任何处理 2.模板偏特化(局部特化)

    可以接受任意指针类型

    // A template<typename T> class A<T *> {}; // 类模板A的偏特化版本,在A后指出特化的范围 3.模板全特化

    指定接受int类型

    template<> class A<int> {} // 类模板A的全特化版本(已经是类模板的一个实例了),在A后直接指出明确类型int 4.例子: #include <iostream> #include <string> using namespace std; template<typename T> class A { public: A() { cout << "R" << endl; } }; /* 注释1 -- 接受指针 template<typename T> class A<T *> { public: A() { cout << "A" << endl; } }; */ /* 注释2 -- 接受int template<> class A<int> { public: A() { cout << "int" << endl; } }; */ int main() { A<string> r; // 1 A<char *> a; // 2 A<int> i; // 3 return 0; } 5.测试不同情况:

    当只有一个类模板(可接受任意类型R)存在时,1, 2, 3都使用类模板实例化

    v2-a53836d6d2a913d69c2fd90b0dd4371f_1440w.png

    注释1是接受所以指针类型(R的子集A),所以称其为类模板A的偏特化(范围特化)。即把类模版A所能接受的指针类型单独处理(实例化)。当取消注释1时:char * 将由这个模板类A的偏特化版本(范围特化,局部特化)处理。

    v2-2bd02b7f6a4dbb5cdbc586f98d76b92b_1440w.png

    注释2只接受int类型的参数(可以看出是全局R中的一个元素)。当你用int实例化A时,将会由这个全特化版本来实现(而不会使用可以接受任意类型的版本(泛化版))。

    v2-6a08120bfe6da8471d8fb374b2665a65_1440w.png

    二、类模板与其模板特化的应用 用模板的偏特化 实现一个 能移除任意类型const属性的模板类:remove_const 主要功能和用法: 功能: 给remove_const一个类型后 --1.(情况1)如果这个类型没有const属性则获得这个类型本身。 --2.(情况2)如果这个类型有const属性则移除它。 用法: remove_const<Type>::type 使用场景: 当拿到一个未知变量时,想获得这个变量(或对象)的非const的类型 1.实现情况1

    似乎没做什么事,aa是int符合情况1。但情况2不符合,bb没有变成int。

    #include <iostream> #include <type_traits> template<typename T> struct remove_const { using type = T; }; int main() { int a = 1; const int b = 2; remove_const<decltype(a)>::type aa = 3; remove_const<decltype(b)>::type bb = 4; std::cout << std::is_same<decltype(aa), int>::value << std::endl; std::cout << std::is_same<decltype(bb), int>::value << std::endl; return 0; }

    运行结果:
    v2-7a36de2df8473699ecc173f27351c876_1440w.png

    2.实现情况2

    从上面的实现可以看出,当传给模板的参数是带const类型时它还会返回带const属性的类型。同时从remove_const的定义也可以看出他是个 复读机 你给他什么类型他就给你什么类型。

    这时候可以使用上面介绍的偏特化的性质,来把带有带const的类型这个子集从全集中分离出来 特殊处理。如下:

    #include <iostream> #include <type_traits> template<typename T> struct remove_const { using type = T; }; template<typename T> struct remove_const<const T> { using type = T; }; int main() { int a = 1; const int b = 2; remove_const<decltype(a)>::type aa = 3; remove_const<decltype(b)>::type bb = 4; std::cout << std::is_same<decltype(aa), int>::value << std::endl; std::cout << std::is_same<decltype(bb), int>::value << std::endl; return 0; }

    运行结果:

    给带const的类型,写了一个特化版本。所以当remove_const接受一个带const的类型时,就会通过这个偏特化版本实例化,由于这个偏特化版本把const从类型中分离出来了,则这里的T就是没有const的类型,从而实现去除类型const的功能。

    v2-d337e4dc338fc9a90fcfa7ef9db27804_1440w.png

    三、模板特化存在性问题

    一个类模板的特化,是对某一个类模板的子集做特化处理的。而它不能"独立存在"。既只有存在一个类模板X, 才能存在对它特化的版本。

    template<typename T> class A<T *> {}; int main() { return 0; }

    v2-1023f2d6ec20e5efa6300629068527d6_1440w.png

  • invoker__qq的博客

    1 主题
    2 帖子
    sunrisepeakS

    @tiansongyu 在 使用AI代理游玩所有任天堂NES游戏(红白机)详细教程----基于gym-retro、pygame、stable-baselines3 中说:

    示例图片 2

    这个中间的游戏 很经典的对站游戏。里面的 能发地波的忍者 和 哪个龙的 大鹏展翅 记忆深刻

  • pinkie ctfer的博客

    3 主题
    7 帖子
    妈耶厥了

    本系列博客作为我学习的一个笔记,注重于代码实现,本人非数学专业,证明能力非常弱,本文掺杂着大量我自己的理解,如有数学大佬光顾,请尽情指正,谢谢
    本系列默认大家都学会了C/C++基本语法

    集合 书上的定义 集合是指具有某种特定性质的不同对象无序聚集成的一个整体。 集合中的每一个对象称为集合的元素; 通常用大写字母表示集合; 用小写字母表示集合中的元素。

    也就是说把一些东西放到一起叫做集合(个人理解)

    代码实现 #include <string> #include <vector> #include <stdint.h> #include <iostream> // 抽象基类,定义集合元素的基本接口 class elmBase { public: // 将元素转换为字节数组的纯虚函数(序列化基础) virtual std::vector<uint8_t> getUint8Array() const = 0; // 默认比较运算符,通过字节数组逐字节比较 virtual int operator==(const elmBase &other) { std::vector<uint8_t> thisDate = getUint8Array(); std::vector<uint8_t> otherDate = other.getUint8Array(); if (thisDate.size() != otherDate.size()) return 0; for (int i = 0; i < thisDate.size(); i++) { if (thisDate[i] != otherDate[i]) return 0; } return 1; } // 转换为字符串表示的纯虚函数 virtual std::string toString() = 0; // 虚析构函数确保正确释放派生类对象 virtual ~elmBase() { } }; // 特殊元素类型,始终返回比较不成立 class alwaysFalse : public elmBase { public: std::vector<uint8_t> getUint8Array() const override { return std::vector<uint8_t>(); // 返回空数组 } std::string toString() override { return {}; // 返回空字符串 } // 重载运算符始终返回false,用于占位符场景 virtual int operator==(const elmBase &other) override { return 0; } ~alwaysFalse() { } }; // 哈希表节点结构,使用链表法解决冲突 using elmNode = struct elmNode { elmBase *elm; // 元素指针 elmNode *next; // 下一个节点指针 }; // 节点工厂函数,创建新节点并初始化元素 elmNode *elmNodeFactory(elmBase *elm) { elmNode *newNode = new elmNode(); newNode->elm = elm; newNode->next = nullptr; return newNode; } // 自定义集合类,基于哈希表实现 class set { private: uint32_t prime; // 哈希表大小(通常选择质数) elmNode **setMap; // 哈希桶数组 // 初始化哈希表结构 void inline _init_(uint32_t prime) { this->prime = prime; this->setMap = new elmNode *[prime]; // 初始化每个桶为alwaysFalse哨兵节点 for (uint32_t i = 0; i < prime; ++i) { this->setMap[i] = elmNodeFactory(new alwaysFalse()); } } protected: // 查找元素对应的哈希桶 elmNode **__find__(elmBase *elm) { uint32_t hash = rotatingHash(elm->getUint8Array(), this->prime); return &(this->setMap[hash]); // 返回桶的指针 } /** * 通用链表操作函数 * @param link 链表头指针的指针 * @param elm 目标元素 * @param cmp 自定义比较函数,返回true时停止遍历 * @param finally 遍历结束后的处理函数(插入等操作) * @return 找到的节点或操作结果节点 */ elmNode *__link__(elmNode **link, elmBase *elm, bool (*cmp)(elmNode *, elmBase *), elmNode *(*finally)(elmNode **, elmNode *, elmBase *) = nullptr) { elmNode *linkPriv = *link; elmNode *linkCurrent = linkPriv; // 遍历链表查找元素 while (linkCurrent != nullptr) { if (cmp(linkCurrent, elm)) { return linkCurrent; } linkPriv = linkCurrent; linkCurrent = linkCurrent->next; } // 执行最终处理函数(如插入新节点) if (finally != nullptr) { return finally(link, linkPriv, elm); } return nullptr; } public: // 构造函数初始化哈希表 set(uint32_t prime) { _init_(prime); } ~set() { for (uint32_t i = 0; i < this->prime; ++i) { elmNode *link = this->setMap[i]; while (link != nullptr) { elmNode *tmp = link; link = link->next; if (tmp != nullptr) { if (tmp->elm != nullptr) { std::cout << "delete " << tmp->elm->toString() << std::endl; delete tmp->elm; } delete tmp; } } } } // 旋转哈希算法实现 static uint32_t rotatingHash(std::vector<uint8_t> key, uint32_t prime) { uint32_t hash = key.size(); for (uint32_t i = 0; i < key.size(); ++i) hash = (hash << 4) ^ (hash >> 28) ^ (uint32_t)(key[i]); return (hash % prime); } // 添加元素到集合 bool add(elmBase *elm, bool force = false) { elmNode **node = __find__(elm); auto finally = [](elmNode **node, elmNode *linkPriv, elmBase *elm) -> elmNode * { // 插入新节点到链表末尾 if (linkPriv->next == nullptr) { linkPriv->next = elmNodeFactory(elm); } return nullptr; }; // 使用lambda作为比较函数 elmNode *targetNode = this->__link__(node, elm, [](elmNode *node, elmBase *tag) -> bool { return *(node->elm) == *tag; }, finally); return targetNode == nullptr; // 返回是否插入成功 } // 检查元素是否存在 bool get(elmBase *elm) { elmNode **node = __find__(elm); elmNode *targetNode = this->__link__(node, elm, [](elmNode *node, elmBase *tag) -> bool { return *(node->elm) == *tag; }); return targetNode != nullptr; } // 移除集合中的元素 bool remove(elmBase *elm) { elmNode **node = __find__(elm); elmNode *ret = this->__link__(node, elm, [](elmNode *current, elmBase *tag) -> bool { // 查找前驱节点执行删除 if (current->next != nullptr && *(current->next->elm) == *tag) { elmNode *tmp = current->next; current->next = current->next->next; delete tmp->elm; // 释放元素内存 delete tmp; // 释放节点内存 return true; } return false; }); return ret != nullptr; } // 调试用打印函数,显示哈希表结构 void print() { std::cout << "[HashTable Structure]" << std::endl; for (int i = 0; i < this->prime; i++) { std::cout << "Bucket " << i << ": "; elmNode *node = this->setMap[i]; while (node != nullptr) { std::cout << node->elm->toString() << " -> "; node = node->next; } std::cout << "NULL" << std::endl; } } }; // 数值类型元素实现 class number : public elmBase { int data; public: number(int data) : data(data) {} std::vector<uint8_t> getUint8Array() const override { // 将整型转换为字节数组 return std::vector<uint8_t>( reinterpret_cast<const uint8_t *>(&data), reinterpret_cast<const uint8_t *>(&data) + sizeof(data)); } std::string toString() override { return std::to_string(data); } ~number() { } }; // 字符串类型元素实现 class string : public elmBase { std::string data; public: string(std::string data) : data(data) {} std::vector<uint8_t> getUint8Array() const override { // 将字符串内容转换为字节数组 return std::vector<uint8_t>(data.begin(), data.end()); } std::string toString() override { return data; } ~string() { } }; 集合与元素的关系 若A表示一个集合,a是集合A中的元素,记作aA,读作a属于A; 若a不是集合A中的元素,则记作aA,读作a不属于A。

    未完待续

  • Maxwell1905的博客

    4 主题
    20 帖子
    SPeakS

    @sky-littlestar 我说的意思就是把你上面的代码 整理一下 用项目(Demo)的方式 放到github, 不是说其他什么写法

  • 冰柠配绿茶的个人博客, 记录探索技术的历程 - Github | X

    2 主题
    8 帖子
    FrozenLemonTeeF

    @SPeak 具体没研究过,不过JetBrains的IDE集成程度比vscode高,可能不太能通过命令或者修改配置文件的方式来修改。如果有朋友有相关了解的可以贴在这里。

  • 个人博客,随想随写

    0 主题
    0 帖子
    没有新主题
  • 个人博客,随想随写 - Doomjustin

    16 主题
    18 帖子
    DoomjustinD

    在前面的写路径优化里,我们已经用 writev 这类手段减少了系统调用的次数。接下来这一篇要解决另一个方向的开销:数据复制

    传统的 send 流程里,用户态的数据会先被复制进内核 socket buffer,再由驱动发给网卡。对于大块数据和高频发送,这一次复制本身就是明显的开销。

    Linux io_uring 的 IORING_OP_SEND_ZC 就是为了避免这一步。但要用对它,需要理解它的完成语义——这是大多数人第一次用时最容易出错的地方。

    1. 为什么需要零拷贝发送

    普通 send 的流程:

    用户 buffer ↓ (内核 memcpy) kernel socket buffer ↓ (DMA 或驱动) 网卡

    SEND_ZC 想跳过中间那一步复制,直接让驱动从用户态内存读取:

    用户 buffer ↓ (直接 DMA,无 memcpy) 网卡

    代价呢?驱动访问内存的时间变长了,所以在这期间,用户态不能释放或改写这块 buffer

    2. 两个 CQE:数据结果 vs 内存释放通知

    SEND_ZC 的完成模式跟普通 send 不一样。它会回两个 CQE:

    第一个 CQE(不带 IORING_CQE_F_NOTIF,带 IORING_CQE_F_MORE)

    内核告诉你这次 send 的结果:成功了多少字节或者失败原因 但驱动仍在使用你的 buffer 协程此时不会恢复

    第二个 CQE(带 IORING_CQE_F_NOTIF,不带 IORING_CQE_F_MORE)

    内核通知:我已经用完你的这块 buffer,可以释放了 协程在这一刻恢复执行 await_resume() 返回第一个 CQE 里已经存好的发送结果

    所以从协程的角度,返回 = notification 已到 = 内核已停止引用你的 buffer。buffer 完全可以释放或改写。

    3. 用 tag type 在 API 层标记零拷贝意图

    如果 async_send_some 直接接收普通 buffer 加一个 bool 标志,很容易在调用点看不出端倪:

    // 危险写法:一眼看不出 buffer 需要特殊处理 co_await socket.async_send_some(payload, true);

    更好的做法是引入一个 tag type,强制显式说明:

    struct ZeroCopyT { std::span<const std::byte> span; }; template<std::ranges::contiguous_range T> auto zero_copy(const T& range) -> ZeroCopyT { return { std::as_bytes(std::span{ range }) }; } // 调用时意图清晰 co_await socket.async_send_some(net::zero_copy(payload));

    这样做的好处:

    类型强制约束:传错类型编译就过不了。 调用点意图清晰:看到 zero_copy(...) 立刻知道这块 buffer 要特殊对待。 完成语义差异明确:零拷贝和普通发送的完成流程完全不同,标签清楚地区分了两条路。 4. Awaiter 的状态机:按 CQE flags 区分两阶段

    对应 SendZCAwaiter 的核心就是这个 complete() 方法,按照 flags 区分 CQE 的含义:

    void SendZCAwaiter::complete(int result, std::uint32_t flags) noexcept { // 不带 NOTIF:这是数据发送结果 CQE,保存结果 if (!(flags & IORING_CQE_F_NOTIF)) set_result(result, flags); // 不带 MORE:标志最后一个 CQE,结束整个操作 if (!(flags & IORING_CQE_F_MORE)) { context().untrack(this); if (handle_) handle_.resume(); } }

    这里的关键点:

    第一个 CQE(不带 NOTIF,带 MORE):存下发送字节数或错误码,不恢复协程 第二个 CQE(带 NOTIF,不带 MORE):跳过结果保存(已有了),恢复协程

    时序是这样的:

    User coroutine SendZCAwaiter io_uring kernel | | | | co_await async_send_some | | | (net::zero_copy(buf)) | | |-------------------------> | | | await_suspend() | | - prepare SQE | | - track(this) | | | submit & wait | | |--------------------------> | | 🔄 (suspended) | | | | 1st CQE: send result | | complete(...) | | !(NOTIF)✓ | set_result(bytes sent) | | (MORE)✓ | (don't resume yet) | | | | | | 2nd CQE: notification | | complete(...) | | !(NOTIF)✗ | (skip set_result) | | (MORE)✗ | untrack(this) | | | handle_.resume() | | 🔄 (awoken) | | | auto result = | | | await_resume() | | | (return byte_sent_) | | | <-- proceed | |

    两个 CQE 都到了,buffer 安全可释放,协程才真正恢复。

    5. 实战边界条件

    虽然协程返回时 notification 已到,但从工程实践角度有几点值得注意:

    Buffer 生命周期的本质约束
    本质上只需要保证 buffer 在协程返回前保持有效。由于协程返回 = notification 已到,这个条件在实际代码中很容易满足。对于栈上的 std::string、std::vector 或其他作用域内存,这不成问题。只有当 buffer 是通过 new 分配且在其他线程被 delete 时,才会违反这个约束——但这种情况通常表明应用层本身的内存管理有问题。

    内存被占用的时间,比想象中还要长(TCP ACK 陷阱)
    第 1 节提到"驱动访问内存的时间变长了",乍一看像是微秒级的 DMA 操作。但在真实场景中,这个"变长"往往不是驱动的事儿,而是毫秒到秒级的网络 RTT。很多网卡驱动和协议栈实现中,内核必须等到对端返回 TCP ACK 确认包,确认数据不需要重传了,才会吐出带 NOTIF 的第二个 CQE。这意味着 SEND_ZC 的 notification 延迟直接和恶劣的物理网络环境强绑定:丢包、重传、网络拥塞都会拉长等待时间。在一个跨洲际链路上发送,notification 可能要等好几秒,期间内存始终被内核占用。这对内存规划和资源隔离的影响不可忽视。

    大文件的"内存锁定"爆炸(RLIMIT_MEMLOCK)
    虽然大文件(MB 级)发送时零拷贝有明显优势,但绝对不能一次性把几个 GB 的大文件全都梭哈给 SEND_ZC。原因是:内核在等待 notification 期间,会把这块物理内存 Pin 住(锁定,防止被 Swap)。如果瞬间提交过大内存,极易触发操作系统的 RLIMIT_MEMLOCK 限制导致直接报错,或者把物理内存撑爆。工业界的正确做法是分片(Chunking):用一个 while 循环,每次 co_await socket.async_send_some(net::zero_copy(chunk)) 发送几 MB,等这几 MB 的 notification 回来(协程唤醒)后,再发下一块。这样既能享受零拷贝的收益,又能保持内存占用在可控范围内。

    错误也要等 notification
    即使第一个 CQE 返回错误,completion 流程也要走完(等 notification)。不要假设出错时内核会跳过 notification。

    示例调用(小消息场景):

    std::string payload = "Zero-copy message from io_uring SEND_ZC\n"; auto result = co_await socket.async_send_some(net::zero_copy(payload)); if (!result) { log::error("send_zc failed: {}", result.error()); co_return; } // 作用域结束时 payload 自动销毁,此时已安全

    大文件分片示例:

    std::ifstream file("large_file.bin", std::ios::binary); const size_t chunk_size = 1024 * 1024; // 1 MB std::vector<char> buffer(chunk_size); while (file.read(buffer.data(), chunk_size)) { size_t bytes_read = file.gcount(); auto result = co_await socket.async_send_some( net::zero_copy(std::span(buffer.data(), bytes_read)) ); if (!result) { log::error("send chunk failed: {}", result.error()); break; } // notification 回来,内存可重用于下一块 } 6. 小结

    SEND_ZC 的核心是一个两阶段完成模型

    第一个 CQE:发送是否成功、发了多少字节 第二个 CQE:内核通知"我用完你的 buffer 了"

    协程在第二个 CQE 到来时才恢复,这样调用方既得到了发送结果,也确切知道何时可以释放 buffer。

    在 API 层用 ZeroCopyT tag type 强制显式选择零拷贝,在 awaiter 层按 CQE flags 正确分发两阶段完成,这两层结合才是鲁棒的设计——既不会因为侥幸巧合而偶然正确,也能清晰表达意图。