色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

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

    文章

    2108

    瀏覽量

    73622
  • 編譯器
    +關注

    關注

    1

    文章

    1623

    瀏覽量

    49108
  • 生成器
    +關注

    關注

    7

    文章

    315

    瀏覽量

    21003

原文標題:C++ coroutine generator 實現筆記

文章出處:【微信號:程序喵大人,微信公眾號:程序喵大人】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    運動控制卡周期上報實時數據IO狀態之C++

    使用C++進行運動控制卡的周期上報功能實現
    的頭像 發表于 12-17 13:59 ?154次閱讀
    運動控制卡周期上報實時數據IO狀態之<b class='flag-5'>C++</b>篇

    C7000 C/C++優化指南用戶手冊

    電子發燒友網站提供《C7000 C/C++優化指南用戶手冊.pdf》資料免費下載
    發表于 11-09 15:00 ?0次下載
    <b class='flag-5'>C</b>7000 <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>優化指南用戶手冊

    TMS320C6000優化C/C++編譯器v8.3.x

    電子發燒友網站提供《TMS320C6000優化C/C++編譯器v8.3.x.pdf》資料免費下載
    發表于 11-01 09:35 ?0次下載
    TMS320<b class='flag-5'>C</b>6000優化<b class='flag-5'>C</b>/<b class='flag-5'>C++</b>編譯器v8.3.x

    C語言和C++中結構體的區別

    同樣是結構體,看看在C語言和C++中有什么區別?
    的頭像 發表于 10-30 15:11 ?199次閱讀

    C7000優化C/C++編譯器

    電子發燒友網站提供《C7000優化C/C++編譯器.pdf》資料免費下載
    發表于 10-30 09:45 ?0次下載
    <b class='flag-5'>C</b>7000優化<b class='flag-5'>C</b>/<b class='flag-5'>C++</b>編譯器

    ostream在c++中的用法

    ostream 是 C++ 標準庫中一個非常重要的類,它位于 頭文件中(實際上,更常見的是通過包含 頭文件來間接包含 ,因為 包含了 和 )。 ostream 類及其派生類(如 std::cout
    的頭像 發表于 09-20 15:11 ?663次閱讀

    OpenVINO2024 C++推理使用技巧

    很多人都使用OpenVINO新版的C++ 或者Python的SDK,都覺得非常好用,OpenVINO2022之后的版本C++ SDK做了大量的優化與整理,已經是非常貼近開發的使用習慣與推理方式。與OpenCV的Mat對象對接方式更是幾乎無縫對接,非常的方便好用。
    的頭像 發表于 07-26 09:20 ?877次閱讀

    C++語言基礎知識

    電子發燒友網站提供《C++語言基礎知識.pdf》資料免費下載
    發表于 07-19 10:58 ?7次下載

    C++實現類似instanceof的方法

    函數,可實際上C++中沒有。但是別著急,其實C++中有兩種簡單的方法可以實現類似Java中的instanceof的功能。 在 C++ 中,確定對象的類型是編程中實際需求,使開發人員
    的頭像 發表于 07-18 10:16 ?574次閱讀
    <b class='flag-5'>C++</b>中<b class='flag-5'>實現</b>類似instanceof的方法

    C/C++中兩種宏實現方式

    #ifndef的方式受C/C++語言標準支持。它不僅可以保證同一個文件不會被包含多次,也能保證內容完全相同的兩個文件(或者代碼片段)不會被不小心同時包含。
    的頭像 發表于 04-19 11:50 ?605次閱讀

    鴻蒙OS開發實例:【Native C++

    使用DevEco Studio創建一個Native C++應用。應用采用Native C++模板,實現使用NAPI調用C標準庫的功能。使用C
    的頭像 發表于 04-14 11:43 ?2594次閱讀
    鴻蒙OS開發實例:【Native <b class='flag-5'>C++</b>】

    使用 MISRA C++:2023? 避免基于范圍的 for 循環中的錯誤

    在前兩篇博客中,我們?向您介紹了新的 MISRA C++ 標準?和?C++ 的歷史?。在這篇博客中,我們將仔細研究以 C++ 中?for?循環為中心的特定規則。
    的頭像 發表于 03-28 13:53 ?785次閱讀
    使用 MISRA <b class='flag-5'>C++</b>:2023? 避免基于范圍的 for 循環中的錯誤

    c語言,c++,java,python區別

    C語言、C++、Java和Python是四種常見的編程語言,各有優點和特點。 C語言: C語言是一種面向過程的編程語言。它具有底層的特性,能夠對計算機硬件進行直接操作。
    的頭像 發表于 02-05 14:11 ?2366次閱讀

    vb語言和c++語言的區別

    VB語言和C++語言是兩種不同的編程語言,雖然它們都屬于高級編程語言,但在設計和用途上有很多區別。下面將詳細比較VB語言和C++語言的區別。 設計目標: VB語言(Visual Basic)是由
    的頭像 發表于 02-01 10:20 ?2258次閱讀

    C++簡史:C++是如何開始的

    MISRA C++:2023,MISRA? C++ 標準的下一個版本,來了!為了幫助您做好準備,我們介紹了 Perforce 首席技術支持工程師 Frank van den Beuken 博士撰寫
    的頭像 發表于 01-11 09:00 ?581次閱讀
    <b class='flag-5'>C++</b>簡史:<b class='flag-5'>C++</b>是如何開始的
    主站蜘蛛池模板: 久久日本精品在线热| 天天国产在线精品亚洲| 在线视频中文字幕| 国产在线播放不卡| 婷婷午夜影院| 富婆夜店找黑人猛男BD在线| 内射人妻无码色AV麻豆去百度搜| 一攻多受高h大总攻| 极品网红液液酱粉嫩福利照子凌酱| 我与恶魔的h生活ova| 囯产愉拍亚洲精品一区| 全身无赤裸裸美女网站| 99欧美精品| 蜜臀色欲AV无人A片一区| 26uuu老色哥| 老师你奶真大下面水真多| 在线播放一区二区精品产| 久久re视频这里精品09免费| 亚洲中文字幕乱倫在线| 九九久久国产精品免费热6| 亚洲性夜色噜噜噜网站2258KK| 国产亚洲精品久久77777| 性生片30分钟| 国产亚洲精品久久精品6| 亚洲444777KKK在线观看| 国产偷国产偷亚州清高| 亚洲精品动漫免费二区| 黑人 尺寸 强行害怕 痛哭| 亚洲麻豆精品成人A在线观看| 果冻传媒在线播放| 亚洲永久免费视频| 久久久欧美国产精品人妻噜噜| 伊人国产精品| 毛片在线不卡| 97视频国产| 秋霞特色大片18岁入口| 纯h超级大尺度小黄文| 天天干夜夜曰| 黑人巨大交牲老太| 有人有片的观看免费视频| 老头操美女|