前往
大廳
主題

C++20 Modules - 讓編譯加速吧 | C++ · 傳統與革新的空間

星雨月奈 | 2022-10-14 22:17:36 | 巴幣 0 | 人氣 531

本篇文章同步發布於 blog

前言


Modules 的好處


以往,假如你在一個 cpp file 中 [code]#include[/code] 了某個 header file,Preprocessor 會把你要的 header file 引入,變成一個 [code]translation unit[/code] ,
但如果你有多個檔案都 [code]#include[/code] 同一個 header,那 Preprocessor 就會每一個都引入一遍,造成編譯速度緩慢

在引入 [code]C++20 Modules[/code] 之後,編譯好的 modules 可以直接在各個地方被 compiler 利用,編譯速度就可以大幅提升。
當然,Modules 帶來好處不只編譯速度的提升,還有封裝、引入順序不影響 macro 等優點。

Compiler 支持狀況


[quote]

[/quote]
至目前為止,只有 MSVC 的支持最完整,GCC , Clang 都只有 partial,因此我們這裡就以 MSVC 舉例。

配置 Visual Studio


在開始之前


在開始之前,我們需要設定一下 Visual Studio,

[quote]
https://en.cppreference.com/w/cpp/compiler_support
C++23 library features > Standard Library Modules

[/quote]
由於 Standard Library Modules 在 C++23 才引入,
為了處理舊有的 header,我們可以使用 [code]header units[/code]

( 其實 msvc 有預先實作,但 IntelliSense 等方面還有些問題,有興趣的人可以自行嘗試 )

Prerequisites


Visual Studio 2019 16.10 or later,這裡我直接用 Visual Studio 2022

Project Properties


  • C++ Language Standard 至少要設定 [code]/std:c++20[/code]
    Configuration Properties > General > C++ Language Standard
  • Scan Sources for Module Dependencies 設定成 [code]Yes[/code]
    Configuration Properties > C/C++ > General

副檔名部分各家要求不太一樣,Visual Studio 要求的是 [code]Example.ixx[/code],
你可以直接在 solution explorer 中加入


概述


這裡先舉個宣告跟實現合在一起的例子

[code]// hello.ixx
export module helloworld; // module declaration

import <iostream>;        // import declaration

export void hello()       // export declaration
{
    std::cout << "Hello world!\n";
}
[/code][code]// main.cpp
import helloworld; // import declaration

int main()
{
    hello();
}
[/code][code]// Console
Hello world!
[/code]

hello.ixx


  • [code]export module helloworld;[/code]
    宣告並匯出一個 module , 名字為 helloworld
    加上 [code]export[/code] 代表這是一個 primary module interface unit
  • [code]import <iostream>;[/code]
    以 [code]header unit[/code] 的方式 [code]import[/code] 一個 header
  • [code]export void hello();[/code]
    要匯出一個 function , 在宣告前加上 [code]export[/code] 即可

main.cpp


  • [code]import helloworld;[/code]
    [code]import[/code] 一個 module , 名字為 helloworld, module 的名字為前面 [code]export module[/code] 的名稱,與檔名無關

Module declarations - 宣告 Module


這裡我們稍微修改一下前一個例子

[code]// hello.ixx
export module helloworld; // module declaration

import <iostream>;        // import declaration

export void hello();     // export declaration
[/code][code]// hello.cpp
module helloworld; // declares a module implementation unit for named module 'helloworld'

// export void hello() // ERROR: a declaration can be exported only from a module interface unit
void hello()       // implementation for 'void hello()'
{
    std::cout << "Hello world!\n";
}
[/code][code]// main.cpp
import helloworld; // import declaration

int main()
{
    hello();
}
[/code]
Module 可以分為以下幾種 module units

  • module interface unit
  • module implementation unit
  • primary module interface unit
  • module partition interface unit
  • module partition implementation unit

module interface unit


module interface unit ,指的是 [code]helloworld.ixx[/code],
module declaration有加上 [code]export[/code],表示其為 [code]interface[/code],相似於之前的 [code].h[/code],

負責宣告、導出 module name , namespaces , functions 等你想導出的東西,
module 的名字為 [code]export module module-name[/code] 的 module-name 所定義,與檔名無關

module implementation unit


module implementation unit ,指的是 [code]hello.cpp[/code],
module declaration 前沒有 [code]export[/code],表示其為 [code]implementation[/code],相似於之前的 [code].cpp[/code],

負責實現 interface file 中的宣告,如同前面所說,名字為 module-name 所定義,與檔名無關。

[quote]
注意,在 [code]implementation[/code] 中也可以進行 [code]import[/code],但 [code]export[/code] 只能在 [code]interface[/code] 中使用。

[/quote]

module partition units


module partition interface unit , module partition implementation unit
可以將單個 module 分成多個 [code]partition[/code],後面會再談到,基本性質跟前面大同小異

primary module interface unit


primary module interface unit ,就這個例子來說,[code]helloworld.ixx[/code] 符合其定義,
module 可能會因為 [code]partition[/code] 的關係有多個 interface ,
primary module interface 每一個 module 都只能也必須有一個
只要他不是宣告為 module partition ( [code]export module A:B;[/code] ),那就會被視為 primary module interface

dot 句點


舉個例子來說,[code]mymodule.mysubmodule[/code]
看到 dot,大家直覺應該都覺得是類似 class,
但在 module 中,dot 並沒有特殊的含義,就只是名稱的一部分,
然而,通常還是會把他拿來表示階層的關係

Exporting declarations - 匯出宣告


[code]export[/code] 除了可以用來匯出 module ([code]export module A;[/code]) 外,也可以匯出宣告namespace
如果不想每個都打 [code]export[/code] ,也可以把東西放在 [code]{ }[/code] 裡,再全部 [code]export[/code] 。

[code]// hello.ixx
export module helloworld; // declares the primary module interface unit for named module 'helloworld'


// zero() will be visible by translations units importing 'helloworld'
export int zero() { return 0; }

// one() will NOT be visible.
int one()  { return 1; }

// Both two() and three() will be visible.
export
{
    int two()  { return 2; }
    int three() { return 3; }
}

// Exporting namespaces also works: number::four() and number::five() will be visible.
export namespace number
{
    int four()  { return 4; }
    int five() { return 5; }
}
[/code]

Importing modules and headers - 匯入 modules 跟 headers


modules and headers


[code]import[/code] 大致可以分為兩種,一種是一般的 [code]import module[/code],另一種是前面提到的 [code]import <headeer>[/code],
擺放的位置,要在 module declaration 後,在其他 declarations 前。

[quote]
在 module 中,不應該直接使用 [code]#include[/code],如果要用,要把它放在 [code]global module fragment[/code]

[/quote]

export-import


透過 module,可以避免我們在 module units 中 [code]import[/code] 的東西,被使用 module 的人連帶 [code]import[/code],
但如果你想,可以透過 [code]export[/code] 將其再次匯出。

[code]// A.ixx (primary module interface unit of 'A')
export module A;

import <iostream>; // import headeer
export import <string_view>; // export-imports

export void print(std::string_view message)
{
    std::cout << message << std::endl;
}
[/code][code]// main.cpp
import A; // import module

int main()
{
    std::string_view message = "Hello, world!";
    print(message);
}
[/code]

Global module fragment


如果因為 [code]macros[/code] 的關係需要用到 [code]#define[/code] 來設定 [code]headers[/code] ,我們可以把它放在 [code]global module fragment[/code] 裡。

位置在 module 的最開始,第一個宣告必須放 [code]module;[/code],範圍到 module declaration 結束。

[code]// A.ixx (primary module interface unit of 'A')
module; // start

// https://learn.microsoft.com/en-us/cpp/c-runtime-library/math-constants?view=msvc-170
// Defining _USE_MATH_DEFINES to use Math Constants
#define _USE_MATH_DEFINES // for C++
#include <cmath>

export module A; // end

import <iostream>;

export void pi()
{
    std::cout << M_PI << std::endl; // 3.1415926
}
[/code]

Private module fragment


如果你想把宣告跟實現在單個檔案中完成,你也可以選擇把實現放在 [code]Private module fragment[/code]。

位置在 module 的最尾端,範圍由 [code]module : private;[/code] 開始。

[code]export module A;

export int one();

module : private; // The start of the private module fragment.

int one()           
{
    return 1;
}
[/code]

Module partitions - 模組分區


最後這部分稍微有點複雜,
如同字面的意思,單個 Module 可以拆為多個 [code]partitions[/code] (分區?),
語法為冒號後加名子,[code]export module module-name:part-name[/code]

  • 命名習慣上,通常是 [code]<primary-module-name>-<module-partition-name>[/code] , ( e.g, [code]A-B[/code] , [code]A-C[/code] )
  • 一個 module partition 只能屬於一個 module,([code]export module A:B;[/code] 屬於 [code]A[/code] )
  • module partition 可以被其他 partition [code]import[/code] , 語法為 [code]import :part-name[/code]

primary module interface unit,除了 implementation,必須 [code]export[/code] 所有 interface partitions
[code]export import :part-name[/code]

[quote]
須 [code]export[/code] 所有 interface partitions規定No diagnostic is required
所以 compiler 不一定會提醒,請注意

[/quote]
[code]// A.ixx
export module A;     // primary module interface unit

export import :B;    // Hello() is visible when importing 'A'.
export import :C;    // WorldImpl() is visible only for 'A.ixx'.

// World() is visible by any translation unit importing 'A'.
export void World()
{
    std::cout << WorldImpl() << '\n';
}

export int zero()
{
    return 0;
}
[/code][code]// A-B.ixx
export module A:B; // partition module interface unit

// import :C;
import <iostream>;

// Hello() is visible by any translation unit importing 'A'.
export void Hello()
{
    std::cout << "Hello" << '\n';
    // std::cout << WorldImpl() << '\n'; // ERROR: WorldImpl() is not visible.
}
[/code][code]// A-C.ixx
export module A:C; // partition module interface unit

// WorldImpl() is visible by any module unit of 'A' importing ':C'.
char const* WorldImpl() { return "World"; }
[/code][code]// main.cpp
import A;

// import <iostream>;

int main()
{

    Hello();
    World();

    // std::cout << zero() << '\n'; // ERROR: 'cout': undeclared identifier
    // WorldImpl(); // ERROR: WorldImpl() is not visible.
}
[/code]
比較特別的是,module partition 一樣可以用 implementation unit 的方式
但是要注意,implementation unit 是不能被 [code]export[/code] 的,

[quote]
另外到目前為止,在 MSVC 下,檔案的 Compile as 需設定成 [code]Module Internal Partition (/internalPartition )[/code]
C/C++ > Advanced > Compile as

[/quote]
[code]//  A.cpp   
export module A;     // primary module interface unit

import :C;           // WorldImpl() is now visible only for 'A.cpp'.
// export import :C; // ERROR: Cannot export a module implementation unit.
[/code][code]// A-C.cpp
module A:C; // partition module **implementation** unit

// WorldImpl() is visible by any module unit of 'A' importing ':C'.
char const* WorldImpl() { return "World"; }
[/code]

結語


Modules 的部分終於暫時寫完了,一開始看好像稍嫌複雜,其實不然,只是相同概念的組合
對於 C++20 的主要 feature ,算是大致介紹了一個,
用一篇就寫完好像還是太多了? 本來想多切成幾篇的說~

References








送禮物贊助創作者 !
0
留言

創作回應

更多創作