CMake构建C++20 Module实例

简介

经典的编译步骤分为三部:

  1. 预处理: 处理预处理指令, 如#include, #define

  2. 编译: 将预处理后的文件编译成汇编代码

  3. 链接: 将汇编代码链接成可执行文件

来看一个简单的例子:

#include <iostream>
int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

我们来观察一下预编译:

root@educative:/usercode# g++ -E main.cpp | wc -c
785655

可以看到预编译后的文件有785655个字符, 也就是说预编译后的文件非常大,
这是因为预编译会对头文件做展开, 这些头文件中可能又包含了更多的头文件,
所以预编译后的文件会非常大.

预处理得到的文件是一个编译单元(Translation Unit).

编译过程是将编译单元编译成汇编代码, 生成的文件叫做目标文件,
目标文件是二进制文件.

链接过程是将目标文件链接成可执行文件, 可执行文件是二进制文件.

这个编译过程继承自C, 对单个编译单元来说这样做是没有问题的,
但是当有许多编译单元时, 就会有很多问题出现.

传统编译过程面临的问题

重复的头文件展开

同一个头文件在不同的编译模块中被使用,则会进行多次展开.
比如有多个源文件均包含<vector>头文件的情形.

编译器宏冲突

C++社区有一个共识就是: 我们应该尽量避免使用宏. 因为宏是一个文本替代,
没有任何C++语义. 考虑下面的例子: 你包含了两个头文件,
这两个头文件中有相同的宏:

// file1.h
#define RED 0XFF0000

在另外一个文件中有如下定义:

// file2.h
#define RED 0XFF0000

那么编译的时候就会有宏定义冲突. 使用了模块之后, 不同的模块是隔离的,
不会有这个问题.

重复包含问题

C++规定在一个编译单元中, 一个符号不能有超过一次的定义. 在程序范围内,
一个函数不能定义超过一次.

为了避免重复定义问题, 头文件一般使用#pragma once 或者 header
guard:

#ifndef _PATH_TO_HEADER_H_
#define _PATH_TO_HEADER_H_
// header file content
#endif

C++ Module(From C++20)

使用C++ Module的好处

  • 模块只会被导入一次, 而且开销可以不计

  • 模块的导入顺序不重要

  • 模块中的重复符号是不允许的

  • 模块可以更好地表达代码的逻辑结构, 可以明确指定导出或不导出的名称,
    也可以将几个模块捆绑成一个更大的模块, 并将其作为一个逻辑包提供给客户

  • 因为有了模块, 就不需要将源代码分为接口和实现部分

模块文件结构

module;  // global module fragment

#include <traditional headers>

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

import <other modules>;

<non-exported declarations>  // names only visible inside the module

export namespace sample {
<exported declarations>  // exported names
}  // 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

CMake对C++模块的支持

根据
CMake官方博客,
3.28版本开始正式支持C++模块. 本文使用的CMakeLists.txt
文件的设置部分如下:

cmake_minimum_required(VERSION 3.28)
project(CppModule CXX)

set(CMAKE_CXX_STANDARD 23)

后续每个例子都会给出其相应的cmake设置.

一个简单的例子

模块文件

export module simple;

export int add(int a, int b) {
    return a + b;
}

export module simple; 是一个模块声明, 它声明了一个模块,
模块名为simple. 在模块中,
我们可以使用export关键字导出函数,命名空间, 类, 变量等.
这样的实体可被其他模块导入.

程序主文件

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

import simple;

int main() {
    return add(1, 2);
}

cmake设置

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

稍微复杂一点的例子

模块文件

module;

#include <string>
#include <utility>

export module complex;

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

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

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

程序主文件

import complex;
#include <iostream>

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

cmake设置

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

模块接口(interface)声明与实现

当模块变得越来越大时, 你可能需要把模块的接口和实现分开.
这与头文件和源文件的分离类似.

模块接口单元

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)

构建环境

本文使用的测试环境:

  • Microsoft Visual Studio 2022 Preview

  • CLion 2024.1 Beta

根据
CMake官方博客,
3.28版本开始正式支持C++模块.
这里给出我这篇博客的CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.28)
project(CppModule CXX)

set(CMAKE_CXX_STANDARD 23)

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


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


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)

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)

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)

完整的文件将在附件中给出, 如有需要可以下载实验.

总结

  • 模块是C++20的新特性, 它可以解决头文件的低效问题.
    导入模块的代价非常低, 并且导入顺序不重要.
    模块还可以解决名称冲突问题.

  • 模块由模块接口单元和模块实现单元组成.
    必须有一个导出模块声明的模块接口单元, 以及任意多个模块实现单元.
    在模块接口中没有导出的名称具有模块链接, 不能在模块外部使用.

  • 模块可以有头文件, 也可以导入和重新导出其它模块.

  • C++20标准库没有模块化.
    使用C++20构建模块化的软件系统是一项具有挑战性的任务.

  • 为了构建大型软件系统, 模块提供了两种方法: 子模块和模块分区.
    与分区不同, 子模块可以独立存在.

  • 由于头文件单元的存在, 可以用导入语句替换包含语句,
    编译器会自动生成一个模块.

  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值