0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C++ coroutine generator实现笔记

程序喵大人 来源:程序喵大人 作者:程序喵大人 2022-12-15 11:03 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

C++20 给我们带来了非常现代化的协程特性,但是主要的增加都集中于语核部分。由于库特性尚未准备充分,所以 C++20 标准库中尚没有多少现成的、组装好的协程设施供我们使用。但是!仅仅通过使用std::coroutine_handle(这是一个编译器为之开洞的类)并在你的类中定制好几个规定的接口,我们就可以组合出五花八门的功能。你可以理解为,虽然我们没有现成的飞机、火车,但是我们有沙子和铁矿石!完全可以从沙子和铁矿石出发,造出飞机、火车。我知道很多人诟病 C++ 的这个特点,没有现成的这个、现成的那个,自己造很麻烦。但是这也是我比较喜欢 C++ 的一点——上限非常高,你可以为自己的飞机、火车做定制,加上你想要的功能或去掉你不想要的功能;除此以外,你甚至还可以造出之前还没有问世的东西,比如星舰!在其他语言中,语言和标准库给你提供了什么就是什么了,你几乎没有超越的能力。

在 C++23 周期,LEWG (库特性工作组) 在新冠肆虐的艰难背景下完成了大量的工作,使得 C++23 增添了不少很有益的设施(可参考 C++23特性总结 - 上 - Mick235711的文章 - 知乎 https://zhuanlan.zhihu.com/p/562383157)。但是,对于协程方面的内容还是举棋不定。std::generator和std::lazy在合并的计划里进进出出,而最终只有std::generator达成目标。对于更花花绿绿的协程库,像task等,则可怜的连提案都没有。

另外,在可预见的将来,哪怕标准库收录了一些基本的协程类,为了探索更加花花绿绿的协程高级功能,我们还是需要从最基本的协程设施出发,也就是理解std::coroutine_handle和相应的接口。

本文将向读者展示如何实现一个简单的生成器 (generator) 和一个能支持协程内外双向信息传递的生成器。网上关于什么是协程的讲解很多,哪怕是讲 C++ 协程的中文材料也不少。且因笔者时间精力有限,本文不会详细地介绍协程概念,而是更侧重于展示怎么实现出一个 generator,以填补相关参考资料的不足。

1

感性的了解一下协程

协程可以简单地理解为:一个执行时可以主动打断,程序执行流程可以在调用方和被调用方之间进进出出若干次的函数。

Python 是最早的一批支持协程的语言,我们不妨用 Python 来演示一下协程的神奇。(其实早在 19 年,那时 C++ 编译器还没支持协程的时候,笔者就是利用 Python 来理解协程的)

c8e7a282-7c19-11ed-8abf-dac502259ad0.jpg

从这个例子我们可以看出以下几点怪异的现象:

1)myrange函数中并没有一句return语句,我们却调用myrange函数得到了一个gen对象(第 22 行)

2) 22 行调用myrange函数后,这个函数似乎并没有立即开始执行(没有执行第 3 行的print语句,倒是执行第 23 行的print语句了)

3) 调用gen对象的__next__方法后,myrange函数开始执行。执行到第 7 行时,myrange函数 "yield" 了一个值,然后程序的执行流程又切换到主函数的第 24 行。__next__方法得到了刚刚 "yield" 的结果。

4) 26 行再次调用__next__时,执行流程回到了myrange中。而且并不是从myrange的开头重新开始执行,而是从上一次 "yield" 的地方,也就是第 7 行继续执行。i 的值似乎也没受到影响。

如果你熟悉了协程的特点,这无非可以概括为,协程执行时可以主动打断(更学术一点叫挂起)自己,将控制权交还给调用方。协程挂起期间,协程的栈上信息都可以得到保留。协程恢复后,从上一次的挂起点继续执行。

经过封装后的 C++ 协程库,也可以向用户展示出和 Python 中几乎完全一致的用法。如下就是我们将要实现的generator所展示的应用效果。

c8f7c4b4-7c19-11ed-8abf-dac502259ad0.jpg

当然,C++ 毕竟是一个静态类型的语言,除了range函数要写 “返回值”generator(当然实际上range是一个协程,代码 81 行只是借用了原来的函数和返回值语法了,严格来说不能认为这里声明了一个返回generator类型的函数) ,以及 90 行需要声明gen的类型以外, 可以说和 Python 没什么两样了。

2

std::coroutine_handle

std::coroutine_handle类模板是为我们实现协程的各种“魔法”提供支持的最底层的设施,其主要负责存储协程的句柄。它分为模板std::coroutine_handle和模板特化std::coroutine_handle,std::coroutine_handle可以简单理解为就是做了类型擦除的std::coroutine_handle

打开头文件我们不难看出,std::coroutine_handle中的不少方法是依靠 __builtin 内建函数,也就是编译器开洞实现的。

c9117f76-7c19-11ed-8abf-dac502259ad0.jpgc939f500-7c19-11ed-8abf-dac502259ad0.jpg

std::coroutine_handle中保存了协程的上下文。我们协程执行到哪儿切出去了(协程切换回来后从哪儿开始继续执行)?我们协程的栈上变量在协程切出去期间怎么能得到保留?这些问题的解决都是归功于std::coroutine_handle保存了协程的上下文。

std::coroutine_handle中的方法不多,但是各个都至关重要。由于有些概念还没有铺垫,我们先只罗列三个比较容易理解的方法:

done方法,可以查询一个协程是否已经结束;

resume方法可以恢复一个协程的执行;

destroy方法可以销毁一个协程。

std::coroutine_handle只是一个很底层很底层的设施,没有 RAII 包裹。它就像裸指针一样(其实它内部也就是一个裸指针),需要靠我们手动创建、手动销毁。我们刚刚谈到,std::coroutine_handle保存了协程的上下文,其中就有栈上变量的信息。如果一个 handle 被创建出来,用完以后我们忘了对它调用 destroy 了,那么其中存储的上下文信息当然也就没有被销毁——也就是内存泄漏了。如果不小心做了两次 destroy,那么就可能会引发 double free 错误了。所以,我们得写一个 RAII 类将其包装起来,以解决忘记销毁或者其他比如浅拷贝等问题。

这里,郑重向大家推荐由清华大学出版社出版的《C++20 实践入门》和《C++20 高级编程》。这两本书是目前最新的一批介绍 C++20 的教程。该书紧跟潮流,就 C++20 的几大热点内容,如modules、concepts、ranges等作了详细介绍。全卷内容质量上乘,精雕细琢,非那些在历史旧版本的基础上草草加两章节新内容圈钱的书可比也!

非常感谢清华大学出版社对这篇文章的赞助!本着对我的读者负责的态度,我坚持要求审读完书的大部分内容后才能做推荐,清华大学出版社的编辑对此给予了高度支持。且,从 8 月份联系我开始,到本文落笔,编辑非常宽容地给了我 4 个月的时间 —— 一段非常充足的时间阅读了这两本书,之后才有了这里的精心推荐。再次表示致敬和谢意!

3

generator

我们的generator类终于出场了。

首先,C++ 的协程要求generator中必须有promise_type这个类型。你可以通过typedef/using的方式 alias 一个别名,也可以干脆就定义成generator的内部类 —— 本文选择后者。

template 
struct generator
{
    struct promise_type
    {
    }
};

promise_type中有这么几个可定制的、会影响协程行为的重要接口,先介绍两个:

1)initial_suspend—— 它回答的是协程一出生时是否就要马上挂起的问题;

2)final_suspend—— 它回答的是协程最后一次是否要挂起的问题;

对于一个generator而言,这两个问题的回答是:

初始时始终都要挂起,最后一次始终都不挂起。

std::suspend_alwaysstd::suspend_never是标准库里面已经定义好的类型,可以方便地回答是否要挂起的问题。

    struct promise_type
    {
        ...

        std::suspend_always initial_suspend() const
        {
            return {};
        }

        std::suspend_never final_suspend() const noexcept
        // 这里注意一下,由于 final_suspend 在收尾阶段工作,所以必须是 noexcept 的
        {
            return {};
        }
    }

在新协程创建完毕好后,C++ 会执行co_await promise.initial_suspend(),同样的, 在协程结束前也会co_await promise.final_suspend()。当然了,从名字中我们也能看出,co_await一个std::suspend_always时,执行流程永远都会无条件切出去,而对于std::suspend_never则是永远也不会切出。

还记得我们刚刚观察的 Python 代码中的现象吗?主函数中调用myrange的时候,是不是立马得到一个gen对象的?是不是myrange里面没有立即得到执行的?在第一次调用__next__的时候才会去执行的吧?这其实就是因为myrange协程一创建好就挂起自己将程序流程切回到调用方了。

如果initial_suspend这里回答的是suspend_never,那么协程就会立刻开始执行。

建议等generator实现完成后读者自己动手实践下,将initial_suspend和final_suspend的回答换换,看看结果会有什么改变。

3)promise_type中第三个定制接口是unhandled_exception,它回答的是协程被其里头的没有捕获的异常终止时做何处理的问题。

我们这里只是简单处理一下,调用exit提前终止程序。当然这样的做法太简化了,实际应用时可以考虑使用std::exception_ptr等设施做更严谨的处理。

    struct promise_type
    {
        ...

        void unhandled_exception()
        {
            std::exit(EXIT_FAILURE);
        }
    }

4)promise_type中第四个定制接口,也是最核心的一个是get_return_object。这个方法也涉及到了如何创建一个 coroutine 的问题 —— 答案就是使用std::coroutine_handle::from_promise(*this),即从自己这个 promise ( 也就是*this) 创建一个 coroutine (from_promise得到的就是一个coroutine_handle)。generator中也需要配合,提供一个接受coroutine_handle类型的构造函数,将刚刚构造出的coroutine_handle保存。

现在,通过get_return_object得到了return_object,就会开始询问是否要做initial_suspend了 (刚刚介绍的initial_suspend还记得吗?)

template 
struct generator
{
    struct promise_type;

    std::coroutine_handle handle;

    struct promise_type
    {
        ...
        generator get_return_object()
        {
            return generator{std::coroutine_handle::from_promise(*this)};
        }
    };

    generator(std::coroutine_handle handle) :
        handle(handle)
    {
    }

    ...
};

我们之前也提到,coroutine_handle是无 RAII 的,因此generator中得根据三/五法则,做好 RAII。该析构的析构,禁止拷贝,写好移动构造/移动赋值。

template 
struct generator
{
    struct promise_type;

    std::coroutine_handle handle;

    ...

public:
    generator() = default;
    generator(const generator &) = delete;

private:
    generator(std::coroutine_handle handle) :
        handle(handle)
    {
    }

public:

    generator(generator && src) :
        handle(src.handle)
    {
        src.handle = nullptr;
    }

    generator& operator=(const generator &) = delete;

    generator& operator=(generator && src)
    {
        if (!handle) {
            handle.destroy();
        }
        handle = src.handle;
        src.handle = nullptr;
    }

    ~generator()
    {
        if (!handle) {
            handle.destroy();
        }
    }

    ...
};

5) 定制yield_value接口

接下来的定制则对于generator来说至关重要,我们马上就可以让我们的generator支持 yield 了!

c9b3baac-7c19-11ed-8abf-dac502259ad0.png

还是以此举例,co_yield关键字实际上只是一个语法糖,这一行会被编译器替换为co_await promise.yield_value(i),在有了initial_suspend和final_suspend的经验后,我们这次也就能很容易地猜测出,我们要在promise_type中实现一个yield_value方法,而返回值负责回答要不要切出的问题。显然,每次 yield 时总是要挂起协程,所以,yield_value方法的返回值类型应当是suspend_always。你猜对了吗?

    struct promise_type
    {
        ....

        std::optional opt;

        template 
        std::suspend_always yield_value(Arg && arg)
        {
            opt.emplace(std::forward(arg));
            return {};
        }
    };

在promise中,我们还增加了一个optional,用以存放 yield 的结果。注意,很多例子,甚至包括 cppreference 在内,promise内部都是用的T类型的裸值来存放 yield 的结果的。在模板编程中这样做兼容性不太好,我们需要考虑能照顾到不可默认构造的类型。除此以外,我们使用万能引用和完美转发以提升构造值时的性能。

而这个opt,当然是在等generator来取它的。

template 
struct generator
{
    ...

    T & next()
    {
        handle.resume();
        if (handle.done()) {
            throw generator_done("generator done");
        }
        return *(handle.promise().opt);
    }
};

generator range(int n)
{
    for(int i = 0; i < n; ++i) {
        co_yield i;
    }
}

int main()
{
    generator gen = range(4);

    for (int i = 0; i < 4; ++i) {
        std::cout << gen.next() << std::endl;
    }
}

这里需要结合前文介绍过的内容梳理下。由于initial_suspend的返回值是suspend_always,所以协程刚创建好后就切出,执行流程到了gen = range(4)。

再下面,每次对gen调用next方法时,会执行handle.resume()恢复协程。

协程首次恢复运行,当然是从range函数的开头开始执行 (如果不是首次恢复运行,当然就是从上一次 yield 出去的地方恢复运行),直到碰上了co_yield。这时, 调用promise.yield_value(i),根据co_yield后面值 (也就是i) 构造好了值保存在opt中。随后,由于promise.yield_value(i)的结果是suspend_always,所以协程切出, 执行流程回到了handle.resume()之后。正常情况下 (协程没有执行完毕),next方法就会从promise里的那个optional中取出 yield 的结果,返回给主函数中以供输出。如果检测到已经是最后一次 yield 后再调用next的 (即 resume 后检测到 done 的话),则抛出generator_done异常。

完整的generator代码如下:

#include 
#include 
#include 
#include 
#include 

struct generator_done :
        std::logic_error
{
    private:
        typedef std::logic_error super;
    
    public:
        using super::super;
};

template 
struct generator
{
    struct promise_type;

    std::coroutine_handle handle;

    struct promise_type
    {
        std::optional opt;

        std::suspend_always initial_suspend() const
        {
            return {};
        }

        std::suspend_never final_suspend() const noexcept
        {
            return {};
        }

        void unhandled_exception()
        {
            std::exit(EXIT_FAILURE);
        }

        generator get_return_object()
        {
            return generator{std::coroutine_handle::from_promise(*this)};
        }

        template 
        std::suspend_always yield_value(Arg && arg)
        {
            opt.emplace(std::forward(arg));
            return {};
        }
    };

public:
    generator() = default;
    generator(const generator &) = delete;

private:
    generator(std::coroutine_handle handle) :
        handle(handle)
    {
    }

public:
    generator(generator && src) :
        handle(src.handle)
    {
        src.handle = nullptr;
    }

    generator& operator=(const generator &) = delete;

    generator& operator=(generator && src)
    {
        if (!handle) {
            handle.destroy();
        }
        handle = src.handle;
        src.handle = nullptr;
    }


    ~generator()
    {
        if (!handle) {
            handle.destroy();
        }
    }

    T & next()
    {
        handle.resume();
        if (handle.done()) {
            throw generator_done("generator done");
        }
        return *(handle.promise().opt);
    }

};

generator range(int n)
{
    for(int i = 0; i < n; ++i) {
        co_yield i;
    }
}

int main ()
{
    generator gen = range(5);

    for (int i = 0; i < 5; ++i) {
        std::cout << gen.next() << std::endl;
    }

}

4

能双向传递信息的

bigenerator

我们目前完成的generator只能做到协程内部向外部 yield 一个值,传递出来信息。能不能做到外部也向协程内部回复一个值,将信息由外部传递到协程内部呢?C++ 的协程机制也是允许的。

其实,co_yield表达式,当然,我们上面也知道了, 其实就是co_await promise.yield_value()这个表达式,其实也是有计算结果的,只不过,我们之前generator中的例子,计算结果为void类型 —— 没有返回值罢了。

要想整个表达式有返回值,当然我们得从promise.yield_value()的返回值入手。我们以前用的是std::suspend_always,现在得自己配置了。

先上效果:

c9db2380-7c19-11ed-8abf-dac502259ad0.jpg

再上源码:

#include 
#include 
#include 
#include 
#include 

struct generator_done :
        std::logic_error
{
    private:
        typedef std::logic_error super;
    
    public:
        using super::super;
};

template 
struct bigenerator
{
    struct promise_type;

    std::coroutine_handle handle;


    struct awaitable : public std::suspend_always
    {
        std::variant * ref;

        U & await_resume() const
        {
            return std::get(*ref);
        }
    };


    struct promise_type
    {
        std::variant var;

        std::suspend_always initial_suspend() const
        {
            return {};
        }

        std::suspend_never final_suspend() const noexcept
        {
            return {};
        }

        void unhandled_exception()
        {
            std::exit(EXIT_FAILURE);
        }

        bigenerator get_return_object()
        {
            return bigenerator{std::coroutine_handle::from_promise(*this)};
        }

        template 
        awaitable yield_value(Arg && arg)
        {
            var.template emplace(std::forward(arg));
            return awaitable{.ref = &var};
        }
    };

public:
    bigenerator() = default;
    bigenerator(const bigenerator &) = delete;

private:
    bigenerator(std::coroutine_handle handle) :
        handle(handle)
    {
    }

public:
    bigenerator(bigenerator && src) :
        handle(src.handle)
    {
        src.handle = nullptr;
    }

    bigenerator& operator=(const bigenerator &) = delete;

    bigenerator& operator=(bigenerator && src)
    {
        if (!handle) {
            handle.destroy();
        }
        handle = src.handle;
        src.handle = nullptr;
    }


    ~bigenerator()
    {
        if (!handle) {
            handle.destroy();
        }
    }

    template 
    T & next(Args&& ... args)
    {
        handle.promise().var.template emplace(std::forward(args)...);
        handle.resume();
        if (handle.done()) {
            throw generator_done("generator done");
        }
        return std::get(handle.promise().var);
    }

};

bigenerator range(int n)
{
    for(int i = 0; i < n; ++i) {
        std::string sunk = co_yield i;
        std::cout << sunk << std::endl;
    }
}

int main ()
{
    bigenerator gen = range(10);

    for (int i = 0; i < 5; ++i) {
        std::cout << gen.next(i + 1, 'a') << std::endl;
    }
}
 

然后讲解:

主要变动就是一个新的内部类:awaitable,在bigenerator中,yield_value接口返回的就是这个类型。它继承自std::suspend_always,表明我们确实还是需要每次 yield 时都要挂起,但是,这里我们重写了await_resume方法,使得协程在恢复时调用这个方法,从 promise 中取出外界传递进去的结果。

    struct awaitable : public std::suspend_always
    {
        std::variant * ref;

        U & await_resume() const
        {
            return std::get(*ref);
        }
    };

下面的代码片段展示了yield_value中怎么构造awaitable。其实只要告知 promise 中的variant的地址就可以了。bigenerator中改用了variant,主要是考虑到 yield 出去的值和 resume 时传递进来的值不会在同一时刻存在,使用variant有助于节省空间。

    struct promise_type
    {
        ...

        std::variant var;

        template 
        awaitable yield_value(Arg && arg)
        {
            var.template emplace(std::forward(arg));
            return awaitable{.ref = &var};
        }
    };

还有bigenerator中next的变化,其实也就是恢复协程前,在 promise 的variant中构造好传进去的对象就好了。

template 
struct bigenerator
{
    ...

    template 
    T & next(Args&& ... args)
    {
        handle.promise().var.template emplace(std::forward(args)...);
        handle.resume();
        if (handle.done()) {
            throw generator_done("generator done");
        }
        return std::get(handle.promise().var);
    }
};

当然,我们这里没有考虑到bigenerator这种传出来和传进去的消息类型都一样的情况,但其实只要做一下偏特化就可以了。由于不是协程部分的技术难点,就不再多介绍了。

全文完。

审核编辑 :李倩

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C++
    C++
    +关注

    关注

    22

    文章

    2122

    浏览量

    76718
  • 编译器
    +关注

    关注

    1

    文章

    1670

    浏览量

    51088
  • 生成器
    +关注

    关注

    7

    文章

    322

    浏览量

    22509

原文标题:C++ coroutine generator 实现笔记

文章出处:【微信号:程序喵大人,微信公众号:程序喵大人】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    C/C++条件编译

    条件编译是一种在编译时根据条件选择性地包含或排除部分代码的处理方法。在 C/C++ 中,条件编译使用预处理指令 #ifdef、#endif、#else 和 #elif 来实现。常用的条件编译指令有
    发表于 12-05 06:21

    C++程序异常的处理机制

    1、什么是异常处理? 有经验的朋友应该知道,在正常的CC++编程过程中难免会碰到程序不按照原本设计运行的情况。 最常见的有除法分母为零,数组越界,内存分配失效、打开相应文件失败等等。 一个程序
    发表于 12-02 07:12

    C/C++代码静态测试工具Perforce QAC 2025.3的新特性

     Perforce Validate 中 QAC 项目的相对/根路径的支持。C++ 分析也得到了增强,增加了用于检测 C++ 并发问题的新检查,并改进了实体名称和实
    的头像 发表于 10-13 18:11 355次阅读
    <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>代码静态测试工具Perforce QAC 2025.3的新特性

    技能+1!如何在树莓派上使用C++控制GPIO?

    在使用树莓派时,你会发现Python和Scratch是许多任务(包括GPIO编程)中最常用的编程语言。但你知道吗,你也可以使用C++进行GPIO编程,而且这样做还有不少好处。借助WiringPi
    的头像 发表于 08-06 15:33 3601次阅读
    技能+1!如何在树莓派上使用<b class='flag-5'>C++</b>控制GPIO?

    C++ 与 Python:树莓派上哪种语言更优?

    Python是树莓派上的首选编程语言,我们的大部分教程都使用它。然而,C++在物联网项目中同样广受欢迎且功能强大。那么,在树莓派项目中选择哪种语言更合适呢?Python因其简洁性、丰富的库和资源而被
    的头像 发表于 07-24 15:32 657次阅读
    <b class='flag-5'>C++</b> 与 Python:树莓派上哪种语言更优?

    Perforce QAC产品简介:面向C/C++的静态代码分析工具(已通过SO 26262认证)

    Perforce QAC专为C/C++开发者打造,支持多种编码规范、功能安全标准(ISO 26262)等,广泛用于汽车、医疗、嵌入式开发领域,可帮助快速识别关键缺陷、提升代码质量、实现合规交付。
    的头像 发表于 07-10 15:57 867次阅读
    Perforce QAC产品简介:面向<b class='flag-5'>C</b>/<b class='flag-5'>C++</b>的静态代码分析工具(已通过SO 26262认证)

    主流的 MCU 开发语言为什么是 C 而不是 C++

    在单片机的地界儿里,C语言稳坐中军帐,C++想分杯羹?难喽。咱电子工程师天天跟那针尖大的内存空间较劲,C++那些花里胡哨的玩意儿,在这儿真玩不转。先说内存这道坎儿。您当stm32f4的256kRAM
    的头像 发表于 05-21 10:33 783次阅读
    主流的 MCU 开发语言为什么是 <b class='flag-5'>C</b> 而不是 <b class='flag-5'>C++</b>?

    C++学到什么程度可以找工作?

    C++学到什么程度可以找工作?要使用C++找到工作,特别是作为软件开发人员或相关职位,通常需要掌握以下几个方面: 1. **语言基础**:你需要对C++的核心概念有扎实的理解,包括但不限于指针、内存
    发表于 03-13 10:19

    创建了用于OpenVINO™推理的自定义C++和Python代码,从C++代码中获得的结果与Python代码不同是为什么?

    创建了用于OpenVINO™推理的自定义 C++ 和 Python* 代码。 在两个推理过程中使用相同的图像和模型。 从 C++ 代码中获得的结果与 Python* 代码不同。
    发表于 03-06 06:22

    源代码加密、源代码防泄漏c/c++与git服务器开发环境

    源代码加密对于很多研发性单位来说是至关重要的,当然每家企业的业务需求不同所用的开发环境及开发语言也不尽相同,今天主要来讲一下c++及git开发环境的源代码防泄密保护方案。企业源代码泄密场景一、在很多
    的头像 发表于 02-12 15:26 891次阅读
    源代码加密、源代码防泄漏<b class='flag-5'>c</b>/<b class='flag-5'>c++</b>与git服务器开发环境

    基于OpenHarmony标准系统的C++公共基础类库案例:ThreadPoll

    1、程序简介该程序是基于OpenHarmony标准系统的C++公共基础类库的线程池处理:ThreadPoll。本案例完成如下工作:创建1个线程池,设置该线程池内部有1024个线程空间。启动5个线程
    的头像 发表于 02-10 18:09 605次阅读
    基于OpenHarmony标准系统的<b class='flag-5'>C++</b>公共基础类库案例:ThreadPoll

    Spire.XLS for C++组件说明

    Spire.XLS for C++ 是一款专业的 C++ Excel 组件,可以用在各种 C++ 框架和应用程序中。Spire.XLS for C++ 提供了一个对象模型 Excel
    的头像 发表于 01-14 09:40 1305次阅读
    Spire.XLS for <b class='flag-5'>C++</b>组件说明

    EE-112:模拟C++中的类实现

    电子发烧友网站提供《EE-112:模拟C++中的类实现.pdf》资料免费下载
    发表于 01-03 15:15 0次下载
    EE-112:模拟<b class='flag-5'>C++</b>中的类<b class='flag-5'>实现</b>

    AKI跨语言调用库神助攻C/C++代码迁移至HarmonyOS NEXT

    )开发框架。它极大地简化了JS与C/C++之间的跨语言访问,为开发者提供了一种边界性编程体验友好的解决方案。通过AKI,开发者可以使用让代码更易读的语法糖,实现JS与C/
    发表于 01-02 17:08

    运动控制卡周期上报实时数据IO状态之C++

    使用C++进行运动控制卡的周期上报功能实现
    的头像 发表于 12-17 13:59 1538次阅读
    运动控制卡周期上报实时数据IO状态之<b class='flag-5'>C++</b>篇