我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

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


2.导出与模块接口

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

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

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

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;
};

3.使用模块

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

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

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

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

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

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

export module person; // 命名模块声明
import ;
export class Person { 

 };

4.标准术语和导出声明

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

export module datamodel;
import ;

export namespace DataModel {
    class Person {  };
    class Address {  };
    using Persons = std::vector;
}

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

export {
    namespace DataModel {
        class Person {  };
        class Address {  };
        using Persons = std::vector;
    }
}

二、模块实现文件

1.分割接口与实现

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

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

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

export module person; // 模块声明
import ;

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; }

2.实现文件的特点

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

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

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

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

三、从实现中分离接口

1.使用头文件时的建议

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

2.模块的不同工作方式

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

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

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

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

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

export module person;
import ;

// 类定义
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; }

四、可见性与可达性

1.引入模块的影响

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

import person;

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

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

const auto& lastName { person.getLastName() };
auto length { lastName.length() };

2.为什么这样工作?

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

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

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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

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

模块的名称可以是任何有效的 C++ 标识符。名称可以包含点,但不能以点开头或结尾,也不能连续包含多个点。
C++模块Module2024-11-30
JavaScript AMD:模块化开发的终极指南
2024-02-18

C++ module编程升级指南,子模块与分区全解析

C++ 标准并没有特别提到子模块,但允许在模块名称中使用点(.),从而可以按任何你想要的层次结构来组织模块。
C++模块编程2024-11-30

Python logging 模块:终极问题解答指南

Python logging 模块提供了强大的功能,可帮助您跟踪、记录和调试应用程序的运行状况。本文将提供一个全面的常见问题解答指南,从模块的基本用法到高级配置和自定义处理程序。
Python logging 模块:终极问题解答指南
2024-03-06

JavaScript 模块化极客指南:解读模块化开发的奥秘

JavaScript 模块化:揭开模块化开发的神秘面纱
JavaScript 模块化极客指南:解读模块化开发的奥秘
2024-02-18

JavaScript CommonJS:模块化开发的终极指南,让你轻松成为开发高手!

JavaScript CommonJS 是一个模块化开发标准,旨在帮助开发者轻松创建和管理模块化代码。本文将介绍 CommonJS 的基础知识、使用指南以及一些常见问题解答,帮助开发者快速上手 CommonJS,迈向开发高手的行列。
JavaScript CommonJS:模块化开发的终极指南,让你轻松成为开发高手!
2024-02-07

C++ 函数库详解:系统功能外延与模块化编程

c++++ 函数库提供预定义函数,可扩展程序功能,简化编程。类型包括标准库 (stl)、平台特定库和第三方库。优点包括代码重用、一致性、功能外延和模块化编程。使用步骤:包含头文件、使用命名空间、调用函数。实战案例:使用 stl 存储和操作数
C++ 函数库详解:系统功能外延与模块化编程
2024-05-03

【Go 基础篇】Go语言函数详解:模块化编程与代码复用

介绍 函数是编程中的基本构建块,用于封装一段代码,使其可以被重复使用。在Go语言中,函数具有丰富的特性,如多参数、多返回值、匿名函数、闭包等,这使得Go语言函数不仅仅是一种执行代码的方式,还是构建模块化程序和实现代码复用的关键工具。本篇博客
2023-08-30

C++ 成员函数详解:对象方法的泛型编程与模板化

c++++ 成员函数是类内部定义的函数,可访问类的数据成员和变量。泛型编程和模板化使代码可重用且独立于数据类型。泛型编程允许编写可用于不同数据类型的代码,而模板化允许创建可用于不同类型成员函数的类。对于需要计算不同形状面积的程序,可使用模板
C++ 成员函数详解:对象方法的泛型编程与模板化
2024-04-29

热门标签

编程热搜

编程资源站

目录