CMake项目使用STDModule

前言

C++20引入了模块功能,用于优化大型项目编译。模块可以加快编译速度、避免宏污染、解决部分ODR问题。而C++23引入了标准库模块,但截止至文章发布/修改时(2025/07/06)依然是实验性支持。

本文章将介绍如何在CMake项目中使用标准库功能,以及一些使用模块时的注意事项。

项目结构

现在创建一个目录,内容如下所示:

项目根目录/

├── CMakeLists.txt

└── main.cpp

然后编写一个最最简单的 main.cpp 代码:

import std;

int main() {
    std::println("Hello, C++23 STD Module!");
}

CMake配置

C++23 模块支持需要较新的 CMake 版本,建议 4.0.0 及以上。

相关事项可以参考 CMake 关于模块支持的 文档 。简而言之,我们需要设置至少四个参数:

  • 生成器设为 Ninja (生成配置时指定)
  • C++版本设为 23
  • 设置 STD Module 的实验性 UUID
  • 设置 CXX_MODULE_STDCMAKE_CXX_MODULE_STD 变量
关于实验性 UUID

查询实验性 UUID 需前往 CMake 的 Github仓库Gitlab仓库 ,在项目的 Help/dev/experimental.rst 文件中,可以找到 C++ import std support 栏目,提供了相关内容。

需要注意的是,此 UUID 与 CMake 版本挂钩,你不能直接复制 master/main 分支下的 UUID,而应该使用和你版本匹配的 Tag 下的 UUID 。你可以在本地通过 cmake --version 命令查看本地CMake版本。

关于 CXX_MODULE_STD 和 CMAKE_CXX_MODULE_STD

这两个变量的相关描述也在实验性支持文档中,和实验性ID位于同一板块,参考上面的内容。

CXX_MODULE_STD 是目标量,建议使用 set_target_properties() 函数设置;而 CMAKE_CXX_MODULE_STD 是全局变量,直接 set()set_property() 命令设置即可。

所以我们的 CMakeLists.txt 可以这样写:

cmake_minimum_required(VERSION 4.0.3)

set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444")

project(hello_std_module)

# 使用全局变量
set(CMAKE_CXX_MODULE_STD 1)
set(CMAKE_CXX_STANDARD 23)

add_executable(main main.cpp)

# 或使用目标参数
# set_target_properties(main PROPERTIES
#     CXX_MODULE_STD 1
#     CXX_STANDARD 23
# )

注意 CMAKE_EXPERIMENTAL_CXX_IMPORT_STD 变量必须在 project() 之前设置,除此方式外也可以使用 CMake 预设或者在命令行使用 -D 附加参数。

如果是 Linux 上的 Clang ,可能需要指定 set(CMAKE_CXX_FLAGS "-stdlib=libc++") 以使用 Clang 自己的标准库 。

Windows 上的 Clang 目前需要使用 LLVM+MinGW 而非MSVC工具链,参考下方“编译工具”部分内容。

编译与运行

然后在生成项目配置文件时指定 Ninja 生成器:

cmake -B build -G "Ninja"

你可能看到类似下面的内容:

CMake Warning (dev) at ......
CMake's support for import std; in C++23 and newer is experimental. It is meant only for experimentation and feedback to CMake developers .......
This warning is for project developers. Use -Wno-dev to suppress it.

-- Detecting CXX compile features - done
-- Configuring done
-- Generating done

虽然它发出了警告,表示模块是实验性的,但已构建完成。

然后可以生成目标文件:

cmake --build build

应该没有任何报错,最后运行:

# Windows
./build/main.exe
# Linux/MacOS
./build/main

你应该能看到正常的打印输出:

Hello, C++23 STD Module!

使用CMake预设

CMake 预设可以与 VSCode / Visual Studio / CLion 进行很好的集成,从而无需手动输入命令进行配置和编译。这里提供一份 CMakePresets.json 的模板:

CMakePresets.json
{
  "version": 10,
  "configurePresets": [
    {
      "name": "ninja-base",
      "hidden": true,
      "description": "config-base, use Ninja, set binaryDir and installDir",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/",
      "installDir": "${sourceDir}/install/",
      "condition": {
        "type": "not",
        "condition": {
          "type": "equals",
          "lhs": "${hostSystemName}",
          "rhs": "Windows"
        }
      }
    },
    {
      "name": "win-msvc",
      "description": "Windows x64 config (MSVC + Ninja Multi-Config)",
      "inherits": "ninja-base",
      "generator": "Ninja Multi-Config",
      "cacheVariables": {
        "CMAKE_CONFIGURATION_TYPES": "Debug;Release",
        "CMAKE_CXX_COMPILER": "cl.exe",
        "CMAKE_C_COMPILER": "cl.exe"
      },
      "architecture": {
        "value": "x64",
        "strategy": "external"
      }            ,
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    },
    {
      "name": "win-clang",
      "description": "Windows x64 config (Clang + MinGW + Ninja Multi-Config), not support STD Module",
      "inherits": "win-msvc",
      "generator": "Ninja Multi-Config",
      "cacheVariables": {
        "CMAKE_CONFIGURATION_TYPES": "Debug;Release",
        "CMAKE_CXX_COMPILER": "clang++",
        "CMAKE_C_COMPILER": "clang"
      }
    },
    {
      "name": "unix-gnu-debug",
      "description": "Linux/macOS GNU Debug config presets(GNU + Ninja)",
      "inherits": "ninja-base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_COMPILER": "g++",
        "CMAKE_C_COMPILER": "gcc"
      }
    },
    {
      "name": "unix-gnu-release",
      "description": "Linux/macOS GNU Release config presets(GNU + Ninja)",
      "inherits": "ninja-base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_COMPILER": "g++",
        "CMAKE_C_COMPILER": "gcc"
      }
    },
    {
      "name": "unix-clang-debug",
      "description": "Linux/macOS Clang Debug config presets(Clang + Ninja)",
      "inherits": "ninja-base",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS": "-stdlib=libc++",
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_COMPILER": "clang++",
        "CMAKE_C_COMPILER": "clang"
      }
    },
    {
      "name": "unix-clang-release",
      "description": "Linux/macOS Clang Release config presets(Clang + Ninja)",
      "inherits": "ninja-base",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS": "-stdlib=libc++",
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_COMPILER": "clang++",
        "CMAKE_C_COMPILER": "clang"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "win-msvc-build-debug",
      "displayName": "win-msvc Build Debug",
      "description": "win-msvc build Debug preset",
      "configurePreset": "win-msvc",
      "configuration": "Debug"
    },
    {
      "name": "win-msvc-build-release",
      "displayName": "win-msvc Build Release",
      "description": "win-msvc build Release preset",
      "configurePreset": "win-msvc",
      "configuration": "Release"
    },
    {
      "name": "win-clang-build-debug",
      "displayName": "win-clang Build Debug",
      "description": "win-clang build Debug preset",
      "configurePreset": "win-clang",
      "configuration": "Debug"
    },
    {
      "name": "win-clang-build-release",
      "displayName": "win-clang Build Release",
      "description": "win-clang build Release preset",
      "configurePreset": "win-clang",
      "configuration": "Release"
    },
    {
      "name": "unix-gnu-build-debug",
      "displayName": "unix-gnu Build Debug",
      "description": "unix-gnu build Debug preset",
      "configurePreset": "unix-gnu-debug",
      "configuration": "Debug"
    },
    {
      "name": "unix-gnu-build-release",
      "displayName": "unix-gnu Build Release",
      "description": "unix-gnu build Release preset",
      "configurePreset": "unix-gnu-release",
      "configuration": "Release"
    },
    {
      "name": "unix-clang-build-debug",
      "displayName": "unix-clang Build Debug",
      "description": "unix-clang build Debug preset",
      "configurePreset": "unix-clang-debug",
      "configuration": "Debug"
    },
    {
      "name": "unix-clang-build-release",
      "displayName": "unix-clang Build Release",
      "description": "unix-clang build Release preset",
      "configurePreset": "unix-clang-release",
      "configuration": "Release"
    }
  ]
}
 

此预设提供了 GNU、MSVC、Clang 的 CMake 基础配置,包括 Clang 可能需要的 -stdlib=libc++ 参数。

关于编译工具

最新编译器与相关工具通常无法通过包管理器安装,下面给出部分安装参考。

Ninja

Ninja 更新频率不高,Linux 中直接使用 apt 等包管理器安装即可,Window 可以去 官网 下载编译好的文件然后设置环境变量。

CMake

建议使用高版本CMake,本文档使用 4.0.3 ,建议 官网 或仓库拉取编译好的文件。

Windows 直接下载MSI安装程序,或下载压缩包并设置环境变量。

Linux 同理,最简单的方式是命令拉取 xxx.sh 安装脚本并执行。

MSVC工具链

使用 Visual Studio Installer ,直接安装社区预览版的 Visual Studio,勾选C++模块相关支持。

Clang-20

Clang 在 Windows 上默认使用MSVC的标准库,但 Clang 启用标准库模块要求 libc++ 或 libstdc++ ,因此无法在 Win 上正常工作。libc++是 Clang 自己的标准库,不支持Windows。所以建议你安装 LLVM-MinGW ,以便在Windows上使用 Clang+GNU 工具链,可正常使用标准库模块。

Linux 下可以正常工作,推荐使用官方脚本安装,下载 llvm.sh

wget https://apt.llvm.org/llvm.sh
# 或者
curl -O https://apt.llvm.org/llvm.sh

然后给予文件可执行权限并运行脚本,此处指定安装 clang 20 的全部内容:

chmod +x llvm.sh
sudo ./llvm.sh 20 all

然后可以检查 clang 版本:

clang++ --version

如果提示找不到 clangclang++ ,可能需要设置软链接:

sudo ln -s /usr/lib/llvm-20/bin/clang-20 /usr/bin/clang
sudo ln -s /usr/lib/llvm-20/bin/clang++ /usr/bin/clang++

GNU-15

参考 [xim+]: 最新gcc15.1.0发布, 一键从源码构建 — c++23 import std启动 | D2learn Forum

关于编辑器

CLion

CLion 已非商业免费,且对 C++23 有较好的智能感知支持,无需额外操作。推荐使用 CMake 预设自动设定 CMake 配置,但你也可以使用 CLion 的手动设置。

VSCode

VSCode 对 CMake 预设也有良好的基础,需要安装 CMake 插件。但是 VSCode 智能感知对 C++23 的支持并不好,需要让 CMake 生成编译数据库,并让 C++ 插件读取此文件。首先在 CMakeLists.txt 中增加代码:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

这样会在构建目录中生成一个文件,包含编译相关的所有命令。然后在项目中,创建 .vscode/settings.json 文件(如果不存在),并添加内容:

{
    "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",
}

然后重新生成配置文件并编译即可(可能需要重启vscode)。遗憾的是,这很可能只对Debug模式生效,Release模式可能依然无法正确进行智能感知。

作者没有尝试过使用 clangd 作为 VSCode 的智能感知插件,你可以自行尝试。

Visual Studio

Visual Studio 的 CMake 项目默认使用 CMake 预设,可以使用上面提供的预设模板。不需要额外操作。

混用模块与头文件

模块文件与头文件在项目中同时出现时很容易有一些问题,尤其是 C++23 后同时具有标准库模块和标准库头文件。很多时候我们不得不用第三方库,这些库往往没有提供模块化的版本,我们必须兼容这些头文件和我们的模块文件同时使用。

后续“模块文件”特指“模块接口文件”,即 .cppm/.ixx 文件。而“模块实现文件”属于 .cpp 文件。

作者建议你的代码满足如下要求,这可以很大程度上避免头文件与模块的冲突问题:

普通源文件

.cpp 文件中,请保证先导入(所有)头文件、后导入(所有)模块:

#include <iostream>
#include ...... // 先导入各种头文件
import std; // 最后导入标准库模块和其他模块
// 可通过编译

如果先导入标准库模块再导入头文件,就会编译失败:

import std;
#include <iostream> // 编译失败,存在重定义问题


模块接口文件与实现文件

对于 .cppm/.ixx 模块接口文件,请将所有头文件都放在全局模块片段中,将模块导入放在正常的声明部分:

module;
// 全局模块片段 导入任何需要的头文件,包括第三方库的头文件
#include <iostream>
export module MyModule;
import std; // 导入其他需要的模块
...... // 正常模块内容

模块实现文件通常也是 .cpp 文件,但它也有全局模块片段,因此导入方式与接口类似:

module;
#include <......> // 先在全局模块片段导入各种头文件
module XXX; // 模块实现文件的模块声明

import std;// 最后导入标准库模块和其他模块
import ...; 
/* 模块中函数的实现代码 */
// ......

分离声明与实现

虽然模块优化了头文件机制,让你可以把实现和声明代码全部放在一个模块文件中,但依然推荐你将二者分离。作者推荐的做法是:将模板、声明和少量内联函数放在 .cppm/.ixx 模块文件中,将函数实现放在 .cpp 文件中,将宏定义放在头文件中。

原因是 .cppm/.ixx 模块接口文件需要按照依赖顺序编译,一个模块文件被修改后,所有依赖它的文件都需要被重新编译一遍,顺序编译又导致整体的编译耗时极高。

.cpp 文件内容的修改不影响模块接口文件,如果你将实现写在 .cpp 文件中,修改了实现代码而未修改声明,则仅需要重新编译此 .cpp 文件(且 .cpp 文件可以轻松并行编译),不需要重新编译大量的模块文件。

举例:

// A.cppm
export module A;
import std;
export struct A{
    void say const { std::println("Hello, A."); }
};

// B.cppm
export module B;
export struct B: public A{ };

// main.cpp
import B;
int main() {
    B b{};
    b.say();
}

如果你像上面一样将函数实现直接写在了模块文件中,功能没有任何问题。但如果你修改了 say 函数的实现,那么需要先重编译 A.cppm 、然后重编译 B.cppm 、最后重编译 main.cpp ,且必须串行编译,因为后者依赖前者。

而且,在此代码风格中,实现代码常常位于模块接口文件中,代码量大导致编译耗时较长,又必须顺序编译,每次修改后都需要花费很长时间重编译项目。

如果你像下面这样分离了声明与实现:

// A.cppm
export module A;
import std; // optional
export struct A{
    void say const;
};

// A.cpp
module A;
import std;
void A::say const { std::println("Hello, A."); }

// B.cppm
export module B;
export struct B: public A{ };

// main.cpp
import B;
int main() {
    B b{};
    b.say();
}

此时修改 say 函数,它位于 .cpp 文件中,不影响 A.cppm 的模块内容,所以只需要重编译 A.cpp 而无需重编译其他文件。

如果你按照这种方式编写代码,后期修改了某个模块接口文件导致大量链式编译,由于它们只含声明和少量内联代码,编译速度会比较快;而受影响的 .cpp 文件们虽然含大量实现代码、却可以轻松的并行编译,整体编译耗时较短。

注意一点,私有模块片段虽然在设计上属于独立翻译单元,但目前的编译参考文件自身的修改,所以修改私有模块片段等于修改模块接口文件自身,也会造成链式编译。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇