首页 > 编程语言 >C++ Module详解,模块化编程终极指南

C++ Module详解,模块化编程终极指南

时间:2024-01-04 12:05:22浏览次数:38  
标签:std string Person 模块接口 C++ person 详解 Module 模块


C++ Module详解,模块化编程终极指南

模块接口文件

定义和扩展名

模块接口文件定义了模块所提供功能的接口。这些文件通常具有 .cppm 扩展名。模块接口以声明文件定义了某个名称的模块开始,这被称为模块声明。模块的名称可以是任何有效的 C++ 标识符。名称可以包含点,但不能以点开头或结尾,也不能连续包含多个点。有效名称的示例包括 datamodelmycompany.datamodelmycompany.datamodel.coredatamodel_core 等。

注意:目前,还没有为模块接口文件标准化的扩展名。然而,大多数编译器支持 .cppm(C++ 模块)扩展名,这也是本书所使用的。请检查你的编译器文档,了解应使用哪种扩展名。

导出与模块接口

模块需要明确声明要导出什么,即客户端代码导入模块时应该可见的内容。从模块导出实体(例如,类、函数、常量、其他模块等)是通过 export 关键字完成的。模块中未导出的任何内容只在模块内部可见。所有导出实体的集合称为模块接口。

以下是一个名为 Person.cppm 的模块接口文件示例,定义了一个 person 模块并导出了一个 Person 类。注意它导入了 <string> 提供的功能。

export module person; // 模块声明
import <string>;      // 导入声明

export class Person   // 导出声明
{
public:
    Person(std::string firstName, std::string lastName)
        : m_firstName { std::move(firstName) }, m_lastName { std::move(lastName) } { }

    const std::string& getFirstName() const { return m_firstName; }
    const std::string& getLastName() const { return m_lastName; }

private:
    std::string m_firstName;
    std::string m_lastName;
};
使用模块

这个 Person 类可以通过导入 person 模块在以下代码中使用(test.cpp):

import person;       // 导入 person 模块声明
import <iostream>;
import <string>;    // 用于 std::string 的 operator<<
using namespace std;

int main() {
    Person person { "Kole", "Webb" };
    cout << person.getLastName() << ", " << person.getFirstName() << endl;
}

所有 C++ 头文件,如 <iostream><vector><string> 等,都是所谓的可导入头文件,可以通过导入声明导入。C++ 中可用的 C 头文件不保证是可导入的。为了安全起见,对于 C 头文件应该使用 #include 而不是导入声明。这样的 #include 指令应该放在所谓的全局模块片段中,它必须在任何命名模块声明之前,并以无名模块声明开始。全局模块片段只能包含预处理指令,如 #include。这样的全局模块片段和注释是唯一允许出现在命名模块声明之前的内容。

例如,如果你需要使用 <cstddef> C 头文件的功能,可以按照以下方式使其可用:

module; // 开始全局模块片段
#include <cstddef> // 包含传统头文件

export module person; // 命名模块声明
import <string>;
export class Person { /* ... */

 };
标准术语和导出声明

在标准术语中,从命名模块声明开始直到文件末尾的一切称为模块视野。几乎任何东西都可以从模块中导出,只要它有一个名称。示例包括类定义、函数原型、类枚举类型、使用声明和指令、命名空间等。如果命名空间使用 export 关键字显式导出,那么该命名空间内的所有内容也会自动导出。例如,以下代码片段导出了整个 DataModel 命名空间;因此,无需显式导出各个类和类型别名:

export module datamodel;
import <vector>;

export namespace DataModel {
    class Person { /* ... */ };
    class Address { /* ... */ };
    using Persons = std::vector<Person>;
}

你还可以使用导出块导出一整块声明。以下是一个示例:

export {
    namespace DataModel {
        class Person { /* ... */ };
        class Address { /* ... */ };
        using Persons = std::vector<Person>;
    }
}

模块实现文件

分割接口与实现

一个模块可以被分割为模块接口文件和一个或多个模块实现文件。模块实现文件通常使用 .cpp 作为扩展名。你可以自由决定将哪些实现移至模块实现文件,以及保留哪些实现在模块接口文件中。

一种选择是将所有函数和方法的实现都移至模块实现文件中,而只在模块接口文件中保留函数原型、类定义等。另一种选择是将小型函数和方法的实现保留在接口文件中,同时将其他函数和方法的实现移至实现文件。在这里,你有很大的灵活性。

模块实现文件同样包含一个命名模块声明,以指定实现是为哪个模块服务的,但没有 export 关键字。例如,之前的 person 模块可以被分割为接口和实现文件,如下所示。这里是模块接口文件:

export module person; // 模块声明
import <string>;

export class Person {
public:
    Person(std::string firstName, std::string lastName);
    const std::string& getFirstName() const;
    const std::string& getLastName() const;

private:
    std::string m_firstName;
    std::string m_lastName;
};

实现现在放在 Person.cpp 模块实现文件中:

module person; // 模块声明,但没有 export 关键字
using namespace std;

Person::Person(string firstName, string lastName)
    : m_firstName { move(firstName) }, m_lastName { move(lastName) } { }

const string& Person::getFirstName() const { return m_firstName; }
const string& Person::getLastName() const { return m_lastName; }
实现文件的特点

请注意,实现文件没有为 person 模块的导入声明。module person 声明隐含地包括了 import person 声明。同样值得注意的是,尽管在方法实现中使用了 std::string,实现文件也没有对 <string> 的任何导入声明。由于隐含的 import person,以及因为此实现文件是同一个 person 模块的一部分,它隐含地继承了模块接口文件中的 <string> 导入声明。

相比之下,向 test.cpp 文件添加 import person 声明并不会隐含地继承 <string> 导入声明,因为 test.cpp 不是 person 模块的一部分。关于这方面有更多内容,在即将到来的“可见性与可达性”一节中进行讨论。

注意:模块接口和模块实现文件中的所有导入声明都必须位于文件顶部,在命名模块声明之后,但在任何其他声明之前。与模块接口文件类似,如果在模块实现文件中需要任何传统头文件的 #include 指令,你应该将它们放在全局模块片段中,其语法与模块接口文件相同。

警告:模块实现文件不能导出任何内容;只有模块接口文件可以。

从实现中分离接口

使用头文件时的建议

当使用头文件(.h)而非模块时,强烈建议只在头文件中放置声明,并将所有实现移至源文件(.cpp)。这样做的一个原因是为了提高编译时间。如果将实现放在头文件中,任何更改,即使只是修改一个注释,也需要重新编译包含该头文件的所有其他源文件。对于某些头文件,这可能会导致整个代码库的全面重新编译。通过将实现放在源文件中,不触及头文件的情况下对这些实现进行修改,意味着只需要重新编译那个单独的源文件。

模块的不同工作方式

模块的工作方式不同。模块接口仅包括类定义、函数原型等,但不包括任何函数或方法的实现,即使这些实现直接位于模块接口文件中。因此,更改模块接口文件内的函数或方法实现,只要不触及接口部分(例如,函数头 = 函数名、参数列表和返回类型),就不需要重新编译使用该模块的用户。

有两个例外:使用 inline 关键字标记的函数/方法,以及模板定义。对于这两者,编译器需要在编译使用它们的客户端代码时了解它们的完整实现。因此,对 inline 函数/方法或模板定义的任何更改都可能触发客户端代码的重新编译。

注意:当头文件中的类定义包含方法实现时,这些方法即使没有标记 inline 关键字,也会被隐式地视为内联。但这对于模块接口文件中类定义中的方法实现不成立。如果这些需要被内联,它们需要被显式地标记为此。

尽管从技术上讲,不再需要将接口与实现分离,但在某些情况下,我仍然建议这样做。主要目标应该是拥有清晰易读的接口。只要函数的实现不会遮蔽接口,使用户难以快速理解公共接口提供了什么,就可以保留在接口中。例如,如果一个模块有一个较大的公共接口,最好不要用实现来遮蔽该接口,这样用户可以更好地了解所提供的内容。然而,小的 getter 和 setter 函数可以保留在接口中,因为它们对接口的可读性影响不大。

从实现中分离接口可以通过几种方式完成。一种选择是将模块分为接口和实现文件,如前一节所讨论的。另一种选择是在单个模块接口文件内分离接口和实现。例如,以下是在单个模块接口文件(person.cppm)中定义的 Person 类,但将实现与接口分离:

export module person;
import <string>;

// 类定义
export class Person {
public:
    Person(std::string firstName, std::string lastName);
    const std::string& getFirstName() const;
    const std::string& getLastName() const;

private:
    std::string m_firstName;
    std::string m_lastName;
};

// 实现
Person::Person(std::string firstName, std::string lastName)
    : m_firstName { std::move(firstName) }, m_lastName { std::move(last

Name) } { }

const std::string& Person::getFirstName() const { return m_firstName; }
const std::string& Person::getLastName() const { return m_lastName; }

可见性与可达性

引入模块的影响

正如之前提到的,当你在非 person 模块的另一个源文件中导入 person 模块(例如在 test.cpp 文件中),你并没有隐含地继承 person 模块接口文件中的 <string> 导入声明。因此,如果没有在 test.cpp 中显式导入 <string>std::string 名称将不可见,意味着以下突出显示的代码行将无法编译:

import person;

int main() {
    std::string str;
    Person person { "Kole", "Webb" };
    const std::string& lastName { person.getLastName() };
}

然而,即使没有向 test.cpp 添加 <string> 的显式导入,以下代码行仍能正常工作:

const auto& lastName { person.getLastName() };
auto length { lastName.length() };
为什么这样工作?

在 C++ 中,实体的可见性和可达性是不同的。通过导入 person 模块,<string> 中的功能变得可达但不可见。可达类的成员函数自动变得可见。这意味着你可以使用 <string> 中的某些功能,例如使用 auto 类型推导将 getLastName() 的结果存储在变量中,并在其上调用诸如 length() 之类的方法。

要使 std::string 名称在 test.cpp 中可见,需要显式导入 <string>。当你想使用例如 operator<< 这样的功能时,也需要这样的显式导入。这是因为 operator<< 不是 std::string 的方法,而是一个非成员函数,只有导入 <string> 后才会变得可见。

cout << person.getLastName() << endl;


标签:std,string,Person,模块接口,C++,person,详解,Module,模块
From: https://blog.51cto.com/u_16062556/9098360

相关文章

  • C++函数模板详解,轻松实现通用函数
    C++函数模板详解,轻松实现通用函数函数模板编写通用函数您也可以为独立的函数编写模板。其语法与类模板类似。例如,您可以编写以下通用函数来在数组中查找一个值并返回其索引:staticconstsize_tNOT_FOUND{static_cast<size_t>(-1)};template<typenameT>size_tFind(const......
  • Elasticsearch7.X Scripting脚本使用详解
    0、题记除了官方文档,其他能找到的介绍Elasticsearch脚本(Scripting)的资料少之又少。一方面:性能问题。官方文档性能优化中明确指出使用脚本会导致性能低;另一方面:使用场景相对少。非复杂业务场景下,基础的增、删、改、查基本上就能搞定。但,不能否认,在解决复杂业务问题(如:自定义评分、自......
  • 【机器学习】常见算法详解第1篇:K近邻 KNN和API使用(已分享,附代码)
    本系列文章md笔记(已分享)主要讨论机器学习算法相关知识。机器学习算法文章笔记以算法、案例为驱动的学习,伴随浅显易懂的数学知识,让大家掌握机器学习常见算法原理,应用Scikit-learn实现机器学习算法的应用,结合场景解决实际问题。包括K-近邻算法,线性回归,逻辑回归,决策树算法,集成学习,聚......
  • 刷题笔记——顺序表(C++)
    665.非递减数列-力扣(LeetCode)给你一个长度为 n 的整数数组 nums ,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。我们是这样定义一个非递减数列的: 对于数组中任意的 i (0<=i<=n-2),总满足 nums[i]<=nums[i+1]。解题思路遍历数组,计算递......
  • C++汇总路径下全部文件名并提取出指定类型或名称的文件
      本文介绍基于C++语言,遍历文件夹中的全部文件,并从中获取指定类型的文件的方法。  首先,我们来明确一下本文所需实现的需求。现在有一个文件夹,其中包含了很多文件,如下图所示;我们如果想获取其中所有类型为.bmp格式的文件的名称,如果文件数量比较多的话,手动筛选就会很麻烦。而借......
  • C++11中的匿名函数用法
    C++11中引用了匿名函数这一个新的特性,它的使用方法如下:[capture](parameters)->return_type{body} 其中:capture 指定了Lambda表达式可以访问的外部变量parameters 是Lambda表达式的参数列表return_type 是返回类型(可选)body 是Lambda函数体下面是一个简单......
  • jmeter module模块控制器
    ModuleController模块控制器:方便测试代码的复用、维护ModuleController的目标参数介绍控制运行测试片段新建测试片段新增ModuleController使用模块控制器调用测试片段引入当前测试计划引入另一个线程组的测试片段模块控制器(ModuleController)提供了......
  • Apache Commons VFS(虚拟文件系统)使用详解
    第1章:ApacheCommonsVFS简介大家好,我是小黑,今天我们来聊聊ApacheCommonsVFS(虚拟文件系统)。想必很多朋友都听说过或者用过ApacheCommons的其他库,但是VFS可能还有点陌生。那么,什么是ApacheCommonsVFS呢?简单来说,它是一个用于处理各种类型文件系统的Java库。不管是本地文件系......
  • 【迅搜12】搜索技巧(二)搜索条件详解
    搜索技巧(二)搜索条件详解上回我们已经学习了一些简单的搜索功能,比如设置搜索语句、分页方法、数量查询以及高亮和折叠的查询效果。而今天,我们将更加深入地学习其它搜索相关的内容。最核心的,就是布尔查询,也就是类似于我们在数据库中的AND和OR之类的语法。不过在这之前,就像是Expl......
  • Pytest测试框架基本使用方法详解
    pytest介绍pytest是一个非常成熟的全功能的Python测试框架,主要特点有以下几点:1、简单灵活,容易上手,文档丰富;2、支持参数化,可以细粒度地控制要测试的测试用例;3、能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests);4、......