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

C++模块分区详解(附带实例)

模块的源码可能会越来越大,以至于难以维护。而且,模块可由逻辑上互相独立的部分组合在一起。为此,模块支持将部分(即分区)组合在一起。模块单元(即导出实体的分区)称为模块接口分区。

然而,也有内部分区不导出任何东西。这样的分区单元被称为模块实现分区。

C++模块分区的使用方式

你可将模块分成如下几个分区:
1) 每个分区单元必须以语句 export module modulename:partitionname;开始。只有全局模块片段先于此声明:
// --- 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 };
    }
}

2) 在主要模块接口单元,导入,然后使用 export import : partitionname 语句导出分区,如以下示例所示:
// --- geometry.ixx/.cppm ---
export module geometry;

export import :core;
export import :literals;

3) 如果模块是从单模块单元构建的,则导入多个分区组成模块的代码将只看到整体模块:
// --- main.cpp ---
import std.core;
import geometry;

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

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

4) 创建不导出任何东西但包含同一模块使用的代码的内部分区是可能的。这样的分区必须以语句 module modulename:partitionname;开始(没有 export 关键字)。不同编译器可能对包含内部分区的文件要求不同的扩展。对于 VC++,扩展必须是 .cpp:
// --- geometry-details.cpp ---
module geometry:details;

import std.core;

std::pair<int, int> split(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 };
}
// --- geometry-literals.ixx/.cppm ---
export module geometry:literals;

import :core;
import :details;

namespace geometry_literals
{
    export int_point operator ""_ip(const char* ptr,
        std::size_t size)
    {
        auto [x, y] = split(ptr, size);
        return { x, y };
    }
}
展示的代码中,geometry 模块分为两个不同分区,分别是 core 和 literals。

当你声明分区时,你必须以 modulename:partitionname 的形式来使用,如 geometry:core 和 geometry:literals。当你导入分区到模块其他地方,这不是必要的。这可从主分区单元 geometry.ixx 和模块接口分区 geometry-literals.ixx 可见。

为了便于阐述,代码片段如下:
// --- geometry-literals.ixx/.cppm ---
export module geometry:literals;

// import the core partition
import :core;

// --- geometry.ixx/.cppm ---
export module geometry;

// import the core partition and then export it
export import :core;

// import the literals partition and then export it
export import :literals;
尽管模块分区是独立的文件,但在使用模块时,它们不作为独立模块或子模块对编译单元可用。它们以单一、聚合模块导出。如果你将 main.cpp 文件中的源码和上文的进行比较,你会看到没有区别。

对模块接口单元而言,包含分区的文件命名没有规则。然而,编译器可要求不同扩展名或支持特定命名规范。例如,VC++ 使用规范 <module-name>-<partition-name>.ixx,该规范用来简化构建命令。

分区,就像模块,可能包含不从模块导出的代码。分区可能完全不导出,它只是内部分区。这样的分区叫模块实现分区。模块声明中没有使用 export 关键字的分区被定义为模块实现分区。

内部分区的一个示例是之前展示的 geometry:details 分区。它提供了帮助函数,称为 split(),将字符串中以逗号分隔的两个整数解析。这个分区然后被导入 geometry:literals 分区,其中 split() 函数用来实现用户自定义字面 _ip。

分区是模块的部分。然而,它们不是子模块。它们在模块外逻辑上不存在。C++ 语言中没有子模块的概念。下面展示的使用分区的代码可以使用模块稍作改写:
// --- geometry-core.ixx ---
export module geometry.core;

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));
}
// --- geometry-literals.ixx ---
export module geometry.literals;

import geometry.core;

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 };
    }
}
// --- geometry.ixx ---
export module geometry;

export import geometry.core;
export import geometry.literals;
在这个示例中,我们有三个模块:geometry.core、geometry.literals 和 geometry。这里,geometry 导入,然后重新导出前两个的所有实体。因此,main.cpp 里的代码不需要更改。单独导入 geometry 模块,我们可以访问 geometry.core 和 geometry.literals 模块的内容。

然而,如果我们不再定义 geometry 模块,那么我们需要显式导入两个模块,如以下代码片段所示:
import std.core;
import geometry.core;
import geometry.literals;

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';
    }
}
选择分区或多个模块来组织你的代码应该取决于你项目的特殊性。如果你使用多个小模块,则你应该对导入有更好粒度的控制。当你开发大型库时这是很重要的,因为用户应该只导入他们使用的(当他们只需要部分功能时,不应导入大型模块)。

相关文章