首页 > 编程笔记 > C++笔记 阅读:24

C++模块的用法(非常详细)

模块是 C++20 中最重要的变化之一。这是 C++ 语言及编写代码、使用代码方式的根本变化。模块在源文件中可用,其从使用它们的编译单元中单独编译。

模块有几个优点,特别跟头文件相比:
在本节中,你将学习如何开始模块的使用。

C++模块的使用方式

当你模块化代码时,可以如下做:
1) 使用 import 指令导入模块,紧跟着模块名。标准库尽管还没模块化,但可作为编译器特定模块存在。

以下代码片段使用了 Visual C++ 的 std.core 模块,它包含了标准库里大多数功能,包括流式库:
import std.core;

int main()
{
    std::cout << "Hello, World!\n";
}

2) 通过创建包含函数、类型、常量和宏的模块接口单元(Module Interface Unit,MIU)来导出模块。它们的声明必须以关键字 export 开头。
VC++ 模块接口单元文件的扩展必须是 .ixx;Clang 接受不同的扩展名,包括 .cpp、.cppm,甚至.ixx。

以下示例导出名为 point 的类模板、用于计算两点距离的 distance() 函数,以及用户自定义字面常量操作符 _ip,其从字符串形式"0,0"或"12,-3"中创建 point 类型的对象:
// --- geometry.ixx/.cppm ---
export module geometry;

import std.core;

export template <class T,
    typename = typename std::enable_if_t<std::is_arithmetic_v<T>,
T>>
struct point
{
    T x;
    T y;
};
export using int_point = point<int>;

export constexpr int_point int_point_zero{ 0,0 };

export template <class T>
double distance(point<T> const& p1,
    point<T> const& p2)
{
    return std::sqrt((p2.x - p1.x) * (p2.x - p1.x) +
        (p2.y - p1.y) * (p2.y - p1.y));
}

namespace geometry_literals
{
    export int_point operator ""_ip(const char* ptr,
        std::size_t size)
    {
        int x = 0, y = 0;
        while (*ptr != ',' && *ptr != ' ')
            x = x * 10 + (*ptr++ - '0');
        while (*ptr == ',' || *ptr == ' ') ptr++;
        while (*ptr != 0)
            y = y * 10 + (*ptr++ - '0');
        return { x, y };
    }
}

// --- main.cpp ---
import std.core;
import geometry;

int main()
{
    int_point p{ 3, 4 };
    std::cout << distance(int_point_zero, p) << '\n';

    {
        using namespace geometry_literals;
        std::cout << distance("0,0"_ip, "30,40"_ip) << '\n';
    }
}

3) 使用 import 指令导入头文件内容。以下示例使用前一示例所示的同一类型和函数:
// --- geometry.h ---
#pragma once
#include <cmath>

template <class T,
    typename = typename std::enable_if_t<std::is_arithmetic_v<T>,
T>>
struct point
{
    T x;
    T y;
};

using int_point = point<int>;

constexpr int_point int_point_zero{ 0,0 };

template <class T>
double distance(point<T> const& p1,
    point<T> const& p2)
{
    return std::sqrt((p2.x - p1.x) * (p2.x - p1.x) +
        (p2.y - p1.y) * (p2.y - p1.y));
}

namespace geometry_literals
{
    int_point operator ""_ip(const char* ptr,
        std::size_t size)
    {
        int x = 0, y = 0;
        while (*ptr != ',' && *ptr != ' ')
            x = x * 10 + (*ptr++ - '0');
        while (*ptr == ',' || *ptr == ' ') ptr++;
        while (*ptr != 0)
            y = y * 10 + (*ptr++ - '0');
        return { x, y };
    }
}

// --- main.cpp ---
import std.core;
import "geometry.h";

int main()
{
    int_point p{ 3, 4 };
    std::cout << distance(int_point_zero, p) << '\n';

    {
        using namespace geometry_literals;
        std::cout << distance("0,0"_ip, "30,40"_ip) << '\n';
    }
}

深度剖析C++模块

模块单元由几个部分组成,强制或可选:
下图展示了模块包含之前提及的所有部分。在左边,我们有模块的源码;在右边,我们解释了模块部分。


图 1 模块(左边)示例,每部分高亮且被解释(右边)

模块可导出任何实体,如函数、类和常量。每次导出都以 export 关键字在前面。这个关键字通常是第一个关键字,在其他 class/struct、template 或 using 前。

前面的 geometry 模块中提供了几个示例:
使用模块而不使用头文件的编译单元,除了将 #include 预处理器指令替换为 import 指令外,不需要其他更改。此外,用同样的 import 指令,头文件也可如模块一样导入,就如之前示例所示。

模块和命名空间之间没有关系。这两个是正交的概念。模块 geometry 导出用户自定义字面""_ip到命名空间 geometry_literals,同时模块中其他的导出在全局命名空间可用。

模块名称和单元文件名称也没有关系。尽管任何文件名有同样的效果,geometry 模块在 geometry.ixx/.cppm 文件中定义。建议你遵循一致的命名规范,使用模块名作为模块文件名。另外,模块单元的扩展名因编译器而异,尽管当模块支持成熟后,将来可能会有变化。

标准库还没模块化,尽管这很可能在标准库未来版本中发生。然而,编译器已经将其模块化实现了。Clang 编译器为每个头文件提供了不同的模块。另外,Visual C++ 编译器为标准库提供了以下模块:
如你从这些模块名(如 std.core 或 std.regex)中所见,模块名可以是一系列由点(.)拼接的标识符。点除了将名称分为代表逻辑层次部分外,如 company.project.module,没有其他作用。相比下划线(如 std_core 或 std_regex),点的使用可以说提供了更好的可读性,下划线也是合法的,和其他一样组成了标识符。

相关文章