CMake构建C++20 Module实例(使用MSVC)

提醒: 本文中的例子是在 MSVC(Microsoft Visual Studio 2022 Preview)编译环境上面测试通过的, 截止文章更新时, 没有在 Clang/GCC 上面验证通过.

背景

在传统的 C++ 编译过程中, 代码的构建通常分为三个主要步骤:

这种流程对于小型项目非常高效, 但随着项目规模的扩大, 其局限性逐渐显现. 比如:

为了解决这些问题, C++20 引入了**模块(Module)**特性, 这是一种替代传统头文件的新方案. 模块具备以下显著优势:

通过模块, C++ 不仅提升了编译效率, 还为大型项目提供了更现代化的代码组织方式. 在本文中, 我们将通过多个实例, 带你逐步掌握 C++20 模块的使用方法, 帮助你从传统的头文件世界迈入模块化的新时代.

编译器对模块的支持

工具最低版本要求
CMake3.28
GCC14.0
Clang/LLVM16.0
MSVC2022 (cl17.4)

目前 MSVC 对 C++20 模块的支持最完善, 而 GCC 和 Clang 尚处于完善阶段, 在实际开发中需要格外注意平台间的兼容性.

本文构建环境

本文中的例子均在 MSVC (Microsoft Visual Studio 2022 Preview) 上测试通过. 尚未在 Clang 和 GCC 上全面验证, 请根据实际需要选择合适的编译器和工具链.

样例GCC 14.2Clang 18MSVC 2022
简单示例
复杂示例
模块接口
子模块
模块分区

源码链接.


模块基础

模块声明

// global module fragment
module;

// 传统头文件, 比如
#include <vector>
#include <string>

// module declaration; starts the module purview
export module module_name;

// 导入其他模块
import std.core;

// 本模块可见的变量, 函数, 类等
int local_var = 42;

// 导出的命名空间
export namespace sample {

// 导出的名字
export constexpr int foo = 42;
export void bar();
export class Baz{};
}  // namespace sample

export命令

有三种方式使用export:

导出声明

export module name;

export int foo(int fir, int sec);

export void bar();

导出组

export module name;

export {
  int foo(int fir, int sec);
  void bar();
}

导出命名空间

export module name;

export namespace name {
int foo(int fir, int sec);
void bar();
}  // namespace name

简单示例

模块文件 simple.ixx

export module simple;  // 模块声明

// 该函数将被导出
export int add(int a, int b) { return a + b; }

export module simple; 是一个模块声明, 它声明了一个模块, 模块名为simple.

在模块中, 我们可以使用export关键字导出函数, 命名空间, 类, 变量等. 这样的实体可被其他模块导入.

主程序 simple_main.cpp

在使用的时候, 使用import simple;导入模块.

import simple; // 导入模块
#include <iostream>

int main() {
  std::cout << "simple add(1, 2)=" << add(1, 2) << "\n";
  return 0;
}

CMakeList.txt

add_library(simple_module)
target_sources(simple_module
        PUBLIC
        FILE_SET CXX_MODULES FILES
        simple.ixx
)
add_executable(simple_demo simple_main.cpp)
target_link_libraries(simple_demo simple_module)

复杂示例

模块文件

请注意如果模块需要引入传统头文件, 需要使用如下写法.

module;

#include <string>
#include <utility>

export module complex;

export namespace complex {
class Person {
 public:
  Person(unsigned age, std::string name) : age_(age), name_(std::move(name)) {}

  [[nodiscard]] std::string Name() const { return name_; }
  [[nodiscard]] unsigned Age() const { return age_; }

 private:
  unsigned age_;
  std::string name_;
};
}  // namespace complex

程序主文件

import complex;
#include <iostream>

int main() {
  complex::Person joy(18, "Joy");
  std::cout << "Name: " << joy.Name() << "\n";
  std::cout << "Age : " << joy.Age() << "\n";
  return 0;
}

cmake 设置

add_library(complex_module)
target_sources(complex_module
        PUBLIC
        FILE_SET CXX_MODULES FILES
        complex.ixx
)
add_executable(complex_demo complex_main.cpp)
target_link_libraries(complex_demo complex_module)

模块接口

当模块变得越来越大时, 可能需要把模块的接口和实现分开. 这样可以更好地组织代码, 并且提高代码的可读性.

模块接口单元

module;

#include <vector>

export module demo;

export namespace demo {

int sum(const std::vector<int>& v);

int prod(const std::vector<int>& v);
}  // namespace demo

模块实现单元

module demo;

#include <numeric>
#include <vector>

namespace demo {
int sum(const std::vector<int>& v) {
  return std::accumulate(v.begin(), v.end(), 1, std::plus{});
}

int prod(const std::vector<int>& v) {
  return std::accumulate(v.begin(), v.end(), 1, std::multiplies{});
}
}  // namespace demo

主程序

import demo;

#include <iostream>
#include <vector>

int main() {
  std::vector vec{1, 2, 3, 4, 5};
  std::cout << "sum  : " << demo::sum(vec) << std::endl;
  std::cout << "prod : " << demo::prod(vec) << std::endl;
}

cmake 设置如下:

add_library(separate_module)
target_sources(separate_module
        PUBLIC
        FILE_SET CXX_MODULES FILES
        separate_interface.ixx
)
target_sources(separate_module PUBLIC separate_impl.ixx)

add_executable(separate separate_main.cpp)
target_link_libraries(separate separate_module)

子模块

对于大的模块可以将其分割为多个子模块. 然后设置一个主模块文件, 导入并导出子模块.

这里的每一个子模块都是一个独立的模块, 可以单独存在.

模块主文件 sort.ixx

设想一下我们目前有一个排序算法库, 包含几种不同的排序算法, 每种排序算法都可以独立使用.

module;

export module sort;
export import sort.bubble_sort;
export import sort.insert;
export import sort.quick;

子模块文件

module;

#include <utility>
#include <vector>

export module sort.bubble_sort;

export namespace sort {
void bubble_sort(std::vector<int>& arr) {
  if (arr.size() < 2) return;
  for (int i = 0; i < arr.size() - 1; i++) {
    auto swapped = false;
    for (int j = 1; j < arr.size() - i; j++) {
      if (arr[j - 1] > arr[j]) {
        std::swap(arr[j - 1], arr[j]);
        swapped = true;
      }
    }
    if (!swapped) break;
  }
}
}  // namespace sort
module;

#include <vector>

export module sort.insert;

export namespace sort {

void insertion_sort(std::vector<int>& arr) {
  for (int i = 1; i < arr.size(); i++) {
    auto k = arr[i];
    int j = i - 1;
    while (j >= 0 && arr[j] > k) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = k;
  }
}

}  // namespace sort

程序主文件

import sort;
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector v{5, 4, 3, 2, 1};
  sort::bubble_sort(v);
  std::cout << std::boolalpha << std::is_sorted(v.begin(), v.end(), std::less{})
            << std::endl;
}

cmake 设置

add_library(sort_module)
target_sources(sort_module
        PUBLIC
        FILE_SET CXX_MODULES FILES
        sort.ixx
        sort_bubble.ixx
        sort_quick.ixx
        sort_insert.ixx
)
add_executable(sort_module_demo sort_main.cpp)
target_link_libraries(sort_module_demo sort_module)

模块分区

模块分区与子模块类似, 唯一的区别是分区模块不能作为一个独立模块存在.

模块主文件

module;

export module partition;
export import :part1;
export import :part2;

子分区文件

module;

#include <utility>
#include <vector>

export module partition:part1;

export namespace sort {
void bubble_sort(std::vector<int>& arr) {
  if (arr.size() < 2) return;
  for (int i = 0; i < arr.size() - 1; i++) {
    auto swapped = false;
    for (int j = 1; j < arr.size() - i; j++) {
      if (arr[j - 1] > arr[j]) {
        std::swap(arr[j - 1], arr[j]);
        swapped = true;
      }
    }
    if (!swapped) break;
  }
}
}  // namespace sort
module;

#include <vector>

export module partition:part2;

export namespace sort {

void insertion_sort(std::vector<int>& arr) {
  for (int i = 1; i < arr.size(); i++) {
    auto k = arr[i];
    int j = i - 1;
    while (j >= 0 && arr[j] > k) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = k;
  }
}

}  // namespace sort

cmake 设置

add_library(partition_module)
target_sources(partition_module
        PUBLIC
        FILE_SET CXX_MODULES FILES
        partition.ixx
        partition_part1.ixx
        partition_part2.ixx
)
add_executable(partition_module_demo partition_main.cpp)
target_link_libraries(partition_module_demo partition_module)

总结

参考链接