前置知识: C++

C++20模块

00:00
4 min Intermediate 2026/6/14

模块系统与import

概述

C++20 引入的模块(Modules)是对传统头文件包含机制的革命性改进。模块通过 exportimport 关键字替代 #include 预处理指令,解决了头文件长期存在的编译速度慢、宏污染、重复解析和命名冲突等问题。模块将代码组织为独立的编译单元,每个模块只需编译一次,后续使用时直接加载预编译的二进制接口,大幅提升大型项目的编译效率。

模块并非简单替换头文件的语法糖,而是从根本上改变了 C++ 的编译模型。模块接口与实现分离,未导出的符号对外不可见,实现了真正的封装性。

基础概念

模块与头文件的区别

特性头文件 (#include)模块 (import)
编译方式文本包含,每次重新解析预编译二进制接口,只解析一次
宏泄漏包含的头文件宏会泄漏模块内宏不泄漏
符号可见性所有声明均可见仅导出符号可见
重复包含需要头文件卫士天然避免重复
编译依赖修改头文件导致全部重编译接口不变则无需重编译

模块的组成

  • 模块接口单元(.cppm 或 .ixx):使用 export module 声明,定义模块的公开接口
  • 模块实现单元:使用 module 声明(无 export),提供接口的实现
  • 模块分区:将大模块拆分为多个子单元,如 module :part_name

快速上手

定义模块

// math.cppm -- 模块接口单元
export module math;

// 导出函数,外部可见
export int add(int a, int b) { return a + b; }
export int multiply(int a, int b) { return a * b; }

// 非导出符号,模块私有,外部不可访问
int helper() { return 42; }

使用模块

// main.cpp
import math;

int main() {
    std::cout << add(1, 2) << std::endl;      // 正确:调用导出函数
    // helper();                                 // 错误:未导出,不可访问
    return 0;
}

编译模块

使用 GCC 编译模块的典型命令:

# 先编译模块接口单元,生成 gcm 缓存
g++ -std=c++20 -fmodules-ts -c math.cppm

# 再编译使用模块的源文件
g++ -std=c++20 -fmodules-ts main.cpp math.o -o main

MSVC 的编译方式略有不同:

# MSVC 会自动处理模块依赖
cl /std:c++20 /EHsc math.cppm main.cpp

详细用法

模块接口与实现分离

对于较大的模块,可以将接口声明与实现分开:

// calculator.cppm -- 模块接口单元,仅声明
export module calculator;

export int add(int a, int b);
export int subtract(int a, int b);
export double divide(double a, double b);
// calculator.cpp -- 模块实现单元
module calculator;

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
double divide(double a, double b) { return a / b; }

导出类和模板

// shapes.cppm
export module shapes;

// 导出完整类
export class Circle {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const;
};

// 导出模板
export template<typename T>
T max_value(T a, T b) {
    return (a > b) ? a : b;
}

// 导出命名空间中的所有内容
export namespace constants {
    constexpr double PI = 3.14159265358979;
    constexpr double E = 2.71828182845904;
}

模块分区

大型模块可以通过分区拆分,便于团队协作和增量编译:

// math_core.cppm -- 核心分区
export module math:core;

export int add(int a, int b) { return a + b; }
export int subtract(int a, int b) { return a - b; }
// math_advanced.cppm -- 高级分区
export module math:advanced;

export double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; ++i) result *= base;
    return result;
}
// math.cppm -- 主模块接口,聚合所有分区
export module math;

// 导入并重新导出分区
export import :core;
export import :advanced;

模块内使用头文件

模块可以通过 module; 全局模块片段引入传统头文件:

// utils.cppm
module;  // 全局模块片段开始

// 在全局模块片段中包含头文件,不会泄漏宏
#include <string>
#include <vector>
#include <iostream>

export module utils;

// 可以正常使用头文件中的类型
export void printVector(const std::vector<int>& vec) {
    for (const auto& v : vec) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

常见场景

替换项目中的头文件

将现有头文件逐步迁移为模块:

// 旧方式: config.h
// #pragma once
// struct Config { int width; int height; };

// 新方式: config.cppm
export module config;

export struct Config {
    int width;
    int height;
};

export Config defaultConfig() {
    return Config{.width = 800, .height = 600};
}

封装第三方库

将第三方头文件封装模块,隔离宏污染:

module;
#include <nlohmann/json.hpp>  // 第三方库,包含大量宏

export module json_wrapper;

// 只导出需要的类型,宏不会泄漏到模块外部
export using json = nlohmann::json;

export json parseJson(const std::string& str) {
    return json::parse(str);
}

注意事项

  • 不同编译器模块支持不同,GCC 需要使用 -fmodules-ts 选项,MSVC 和 Clang 各有自己的实现方式,跨编译器项目需注意兼容性
  • 模块接口文件的后缀名不统一:GCC 使用 .cppm,MSVC 使用 .ixx,Clang 使用 .cppm自定义后缀,构建系统需要正确配置
  • import 声明必须翻译的最前(在所有非全局模块的代码之前),不能出现在函数体内
  • 模块内的定义不会泄漏到导入方,这是模块的重要特性,但也意味着依赖宏的代码需要调整
  • 模块编译顺序有依赖关系,构建系统必须先编译依赖模块接口,再编译使用方
  • 头文件模块可以混合使用,通过全局模块 module; 引入头文件,便于渐进式迁移

进阶用法

私有模块分区

使用非导出分区隐藏实现

// math_impl.cppm -- 私有分区,不对外导出
module math:impl;

// 这些函数仅在模块内部可用
int internal_gcd(int a, int b) {
    return b == 0 ? a : internal_gcd(b, a % b);
}
// math.cppm
export module math;
import :impl;  // 导入私有分区,但不重新导出

export int gcd(int a, int b) {
    return internal_gcd(a, b);  // 内部使用私有实现
}
// 外部无法访问 internal_gcd

模块与模板显式实例化

// container.cppm
export module container;

export template<typename T>
class Stack {
    struct Node { T data; Node* next; };
    Node* top_ = nullptr;
public:
    void push(const T& value);
    T pop();
    bool empty() const { return top_ == nullptr; }
};

// 显式实例化声明,减少编译时间
extern template class Stack<int>;
extern template class Stack<double>;

模块与命名空间组合

// network.cppm
export module network;

// 导出整个命名空间
export namespace network::http {
    struct Request {
        std::string url;
        std::string method;
    };

    struct Response {
        int status;
        std::string body;
    };

    Response send(const Request& req);
}

// 模块内部命名空间,不导出
namespace network::detail {
    bool validateUrl(const std::string& url) {
        return !url.empty() && url.substr(0, 4) == "http";
    }
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式