c++模块(Modules)

1.概要

2.内容

C++20引入了模块(Modules)的概念,旨在改善模块化编程的方式。以下是对C++模块的详细解析:

一、基本概念

  • 定义:模块是一组源代码文件,这些文件独立于源文件(或者更确切地说,导入它们的转换单元)进行编译。模块可消除或减少与使用头文件相关的许多问题。
  • 优势:它们通常会减少编译时间,有时甚至会显著减少。在模块中声明的宏、预处理器指令和非导出名称在模块外部是不可见的,不会影响导入模块的转换单元的编译。你可以按任何顺序导入模块,而无需考虑宏重新定义。导入转换单元中的声明不参与导入模块中的重载解析或名称查找。

二、模块接口与实现

  • 模块接口文件:定义了模块所提供功能的接口。这些文件通常具有.cppm扩展名。模块接口以声明文件定义了某个名称的模块开始,这被称为模块声明。模块的名称可以是任何有效的C++标识符。名称可以包含点,但不能以点开头或结尾,也不能连续包含多个点。有效名称的示例包括datamodel、mycompany.datamodel、mycompany.datamodel.core、datamodel_core等。注意,目前还没有为模块接口文件标准化的扩展名。然而,大多数编译器支持.cppm(C++模块)扩展名。
  • 模块实现文件:定义了模块导出的内容。模块的最简单形式可以是一个结合了模块接口和实现的文件。还可以将实现放入一个或多个单独的模块实现文件中,类似于.h和.cpp文件的操作方式。
  • 导出与模块接口:模块需要明确声明要导出什么,即客户端代码导入模块时应该可见的内容。从模块导出实体(例如,类、函数、常量、其他模块等)是通过export关键字完成的。模块中未导出的任何内容只在模块内部可见。

三、模块的使用

  • 创建模块文件:在项目中创建一个新的模块文件(通常以“.ixx”或“.cppm”为扩展名),并在文件中定义模块。
  • 导入模块:在其他文件中可以使用import关键字来导入模块并使用其中定义的函数或类。
  • 编译代码:使用支持模块的编译器编译代码,可以通过命令行或集成开发环境来编译模块化的代码。

四、模块的特性

  • 模块化编程:模块提供了一种新的方式来组织和管理代码,可以提高代码的可重用性。
  • 减少编译时间:模块通过一次编译即可生成中间代码,后续只需引用模块的接口,而不必去重新解析整个头文件。
  • 避免命名冲突:模块使用独立的命名空间,避免了头文件中常见的命名冲突问题。
  • 简化依赖管理:使用模块可以减少编译器需要处理的依赖关系,从而提高编译效率。
  • 预编译模块:C++20还引入了预编译模块的概念,可以将模块编译成二进制格式,以加快后续的编译过程。

五、模块与头文件

  • 并行使用:模块可以与头文件并行使用。C++源文件可以import模块,并同时#include头文件。在一些情况下,可以将头文件导入为模块,这比通过预处理器使用#include处理它更快。建议在新项目中尽可能多地使用模块,而不是头文件。对于正在开发中的大型现有项目,请尝试将旧标头转换为模块。根据能否显著缩短编译时间来确定是否采用。
  • 对比:模块相较于传统的头文件机制,带来了更清晰的依赖关系和更好的性能表现。逐步使用模块,可以更有效地管理大型项目的结构和编译过程。

六、示例

以下是一个简单的模块定义和使用示例:

// math_operations.ixx (模块接口)
export module math_operations;
export int add(int a, int b) { return a + b; }
export int subtract(int a, int b) { return a - b; }

// math_operations.ixx (模块实现) 或者单独的文件 math_operations_impl.cpp
module math_operations;
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }

// main.cpp
import math_operations;
import <iostream>;

int main() {
    std::cout << "Sum: " << add(3, 4) << std::endl;
    std::cout << "Difference: " << subtract(10, 5) << std::endl;
    // multiply 和 divide 不可用,因为它们没有被 export
}

在这个例子中,math_operations模块定义了一个加法函数和一个减法函数,并在接口文件中使用export关键字导出它们。然后,在main.cpp文件中,通过import关键字导入math_operations模块,并使用其中定义的函数。

综上所述,C++模块是C++20引入的一项重要特性,它提供了更高效的模块化编程方式,有助于减少编译时间、避免命名冲突和简化依赖管理。随着编译器对模块支持的不断完善,模块有望在C++项目中得到更广泛的应用。

 3.关联链接

c++模块(Modules)-优快云博客

c++20:概念(Concepts)-优快云博客

c++20:范围(Ranges)-优快云博客

c++20:协程(Coroutines)-优快云博客

c++20:协程(Coroutines)-优快云博客

c++协程(Coroutines)-无限的整数序列-优快云博客

三路比较运算符(Spaceship Operator)-优快云博客

std::format-优快云博客

c++20,位操作增强-优快云博客

c++20 空指针比较-优快云博客

4.关联知识

1.c++20有什么新功能

C++20 是 C++ 标准的一个重要更新,它引入了许多新特性和改进,显著提升了语言的功能性和易用性。以下是一些关键的新功能:

一、模块(Modules)

  • 功能:改变了组织源码文件的方式,旨在替换传统的头文件 #include 模型,以减少编译时间和改善依赖管理。在项目中,不再需要区分 .cpp 和 .h 文件。
  • 示例
// my_module.module
export module my_module;
export void greet() {
    std::cout << "Hello from the module!" << std::endl;
}

// main.cpp
import my_module;
int main() {
    greet();
    return 0;
}

二、概念(Concepts)

  • 功能:允许在编译时精确指定模板参数需满足的条件,增强编译时错误信息和泛型编程的清晰度。
  • 示例
#include <concepts>

template<typename T>
concept Integral = std::is_integral_v<T>;

template<Integral T>
T abs(T x) {
    return x >= 0 ? x : -x;
}

int main() {
    static_assert(abs(42) == 42);
    static_assert(abs(-42) == 42);
    // static_assert(abs(42.0) == 42.0); // 错误,double不满足Integral概念
}

三、范围(Ranges)

  • 功能:扩展了标准库中的算法,支持更简洁、更灵活的序列操作。Ranges 库是对标准模板库(STL)的一个重要扩展,它重新定义了容器和算法的交互方式,使代码更具可读性和表达力。
  • 示例
#include <range/v3/all.hpp>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6};
    auto even = vec | ranges::view::filter([](int x) { return x % 2 == 0; });
    for (int val : even) {
        std::cout << val << " ";
    }
    return 0;
}

四、协程(Coroutines)

  • 功能:正式支持协程,使得编写异步代码更为直观。协程是一种特殊的函数,允许在执行过程中暂停并在稍后恢复。
  • 示例(简化的生成器示例):
#include <coroutine>
#include <iostream>

struct Generator {
    struct promise_type;
    using handle_t = std::coroutine_handle<promise_type>;

    Generator(handle_t h) : coro(h) {}

    ~Generator() {
        if (coro) coro.destroy();
    }

    int next() {
        coro.resume();
        return coro.promise().current_value;
    }

private:
    handle_t coro;
};

struct Generator::promise_type {
    int current_value{0};
    Generator get_return_object() {
        return Generator{handle_t::from_promise(*this)};
    }

    std::suspend_always initial_suspend() { return {}; }

    std::suspend_always final_suspend() noexcept { return {}; }

    void return_value(int value) {
        current_value = value;
    }

    void unhandled_exception() {
        std::terminate();
    }
};

Generator count_up_to(int limit) {
    for (int i = 1; i <= limit; ++i) {
        co_yield i;
    }
}

int main() {
    for (int val : count_up_to(5)) {
        std::cout << val << " ";
    }
    return 0;
}

五、三路比较运算符(Spaceship Operator)

  • 功能:引入了<=>运算符,用于实现综合比较(小于、等于、大于)。
  • 示例
#include <compare>

struct Point {
    int x, y;
    auto operator<=>(const Point&) const = default;
};

int main() {
    Point p1{1, 2}, p2{1, 2};
    if (p1 == p2) std::cout << "Equal" << std::endl;
}

六、std::format

  • 功能:C++20 引入的标准库函数,为字符串格式化提供了统一且强大的接口,类似于 Python 中的 str.format 或 C 的 printf 函数,但更加安全和灵活。
  • 示例
#include <format>
#include <iostream>

int main() {
    auto str = std::format("The answer is {}.", 42);
    std::cout << str << std::endl; // 输出: The answer is 42.
    return 0;
}

七、位操作增强

  • 功能:C++20 对位操作进行了增强,引入了几个新函数来提高位操作的便利性和表达能力。
  • 示例(使用 std::has_single_bit 和 std::countl_zero):
#include <bit>
#include <iostream>

int main() {
    unsigned int num = 0b100000;
    std::cout << "Is power of 2?" << std::boolalpha << std::has_single_bit(num) << std::endl;
    std::cout << "Leading zeros: " << std::countl_zero(num) << std::endl;
}

八、空指针比较

  • 功能:C++20 引入了新的空指针常量 nullptr 与整数类型的比较操作,明确禁止了这种比较,以防止潜在的逻辑错误。以前,比较 nullptr 和整数在某些实现下是允许的,但现在这样的比较会引发编译错误,确保了代码的清晰和安全。
  • 示例(演示非法比较):
void checkPointer(int* ptr) {
    // if (ptr == 0) // 在C++20中,这种比较会被认为是错误的。
    // std::cout << "ptr is null" << std::endl;
}

九、其他改进和新增特性

  • 关键字和语法增强:引入了 co_await、co_return、co_yield 等关键字以支持协程,consteval 用于声明必须在编译时求值的常量表达式函数,constinit 用于声明必须在编译时初始化的变量,以及 inline 变量等。
  • constexpr 支持的扩展:C++20 扩展了 constexpr 的能力,使其可以用于更复杂的表达式和函数,包括虚函数、动态内存分配、try-catch 异常处理等。
  • std::span:一个轻量级的视图类型,表示一段连续内存的子集,它类似于指针和数组,但更安全、更易用。
  • 类型安全和错误检查:通过引入概念(Concepts)和范围(Ranges),C++20 提供了更强的类型安全和更好的编译时错误检查。

总的来说,C++20 的这些新特性和改进使得 C++ 语言更加现代化、强大和易用。

2.c++模块(Modules)中,类如何使用呢

在C++模块(Modules)中,类的使用方式与在传统头文件和源文件中使用类的方式非常相似,但有一些关键的不同点。以下是如何在C++模块中定义、导出和使用类的指南:

1. 定义和导出类

首先,在模块接口文件(.ixx.cppm或你选择的扩展名)中定义并导出类。使用export关键字来确保类在模块外部可见。

// MyModule.ixx
export module MyModule;

export class MyClass {
public:
    MyClass(int value);
    int getValue() const;
    void setValue(int value);

private:
    int m_value;
};

在模块实现文件(可以是同一个文件,也可以是一个单独的.cpp文件)中,提供类的成员函数定义。

// MyModule.cpp (如果与接口文件分开)
module MyModule;

MyClass::MyClass(int value) : m_value(value) {}

int MyClass::getValue() const {
    return m_value;
}

void MyClass::setValue(int value) {
    m_value = value;
}

2. 导入和使用类

在另一个转换单元(通常是另一个.cpp文件)中,导入模块并使用类。

// main.cpp
import MyModule;
#include <iostream>

int main() {
    MyClass obj(42);
    std::cout << "Value: " << obj.getValue() << std::endl;
    obj.setValue(100);
    std::cout << "New Value: " << obj.getValue() << std::endl;
    return 0;
}

3. 注意事项

  • 导出控制:只有使用export关键字导出的类或成员函数才能在模块外部被访问。未导出的部分在模块外部是不可见的。
  • 编译:确保你的编译器支持C++20模块,并且你正在使用支持模块的编译标志(例如,对于GCC和Clang,可能是-std=c++20 -fmodules-ts)。
  • 命名:模块和类的命名应遵循C++的命名规则,避免与标准库或其他模块中的名称冲突。
  • 依赖管理:模块可以帮助你更好地管理依赖关系,因为你可以明确地指定哪些部分是被导出的,哪些部分是私有的。

4. 示例总结

上面的示例展示了如何在模块中定义、导出和使用类。通过这种方式,你可以更好地封装代码,减少编译时间,并避免命名冲突。随着C++20模块特性的成熟和编译器支持的完善,它们将成为C++项目中组织代码的重要工具。

3.c++模块(Modules)中使用类,模块中包含一个导出类和非导出类 

在C++模块(Modules)中,你可以同时包含一个导出类和一个非导出类。导出类将在模块外部可见并可被使用,而非导出类则仅在模块内部可见,用于实现细节或辅助功能。以下是一个简单的示例,展示了如何在模块中同时定义导出类和非导出类:

模块接口文件(例如 MyModule.ixx

export module MyModule;

// 导出类
export class PublicClass {
public:
    PublicClass();
    void doSomething();

private:
    // 非导出类的实例作为私有成员
    class Impl;
    Impl* pImpl;
};

// 注意:这里不导出Impl类
class PublicClass::Impl {
public:
    void doInternalWork();
};

模块实现文件(例如 MyModule.cpp

module MyModule;

#include <iostream>

// 实现导出类的构造函数和成员函数
PublicClass::PublicClass() : pImpl(new Impl) {}

void PublicClass::doSomething() {
    pImpl->doInternalWork();
}

// 实现非导出类的成员函数
void PublicClass::Impl::doInternalWork() {
    std::cout << "Doing internal work..." << std::endl;
}

// 如果需要,可以在这里定义其他非导出类或函数

使用模块的文件(例如 main.cpp

import MyModule;

int main() {
    PublicClass publicObj;
    publicObj.doSomething(); // 这将调用Impl类的doInternalWork方法
    return 0;
}

注意事项

  1. 封装性:通过将实现细节隐藏在非导出类中,你可以增强模块的封装性。这有助于减少模块外部对内部实现的依赖,从而使模块更加健壮和可维护。

  2. 编译隔离:非导出类在模块外部是不可见的,这意味着它们不会增加模块的编译时间,也不会与模块外部的代码产生命名冲突。

  3. PImpl惯用法:上面的示例使用了PImpl(Pointer to Implementation)惯用法,这是一种常见的封装技术,用于隐藏类的实现细节。通过将实现细节放在非导出类中,并通过指针(或智能指针)在导出类中访问它们,你可以实现更好的封装和编译隔离。

  4. 模块支持:请确保你的编译器支持C++20模块,并且你正在使用正确的编译标志来启用模块功能。

通过结合使用导出类和非导出类,你可以更有效地组织和管理C++模块中的代码,同时保持清晰的接口和封装良好的实现。

4.class PublicClass::Impl 

在C++中,class PublicClass::Impl 表示 PublicClass 类内部定义的一个嵌套(或内部)类 Impl。这种嵌套类是一种封装技术,常用于实现所谓的“PImpl”(Pointer to Implementation)惯用法,或者仅仅是为了在逻辑上组织代码。

在 PublicClass::Impl 的上下文中,PublicClass 是外部类,而 Impl 是它的内部类。内部类 Impl 的定义是嵌套在外部类 PublicClass 的定义中的,但它可以具有自己的成员变量和成员函数,就像任何普通类一样。

下面是一个简化的例子,展示了如何在 PublicClass 中定义和使用 Impl 类:

class PublicClass {
public:
    PublicClass();
    ~PublicClass();
    void doSomething();

private:
    // 嵌套类(内部类)的定义
    class Impl {
    public:
        void doInternalWork();
    };

    // 指向内部类实例的指针
    Impl* pImpl;
};

// PublicClass 构造函数的实现
PublicClass::PublicClass() : pImpl(new Impl) {}

// PublicClass 析构函数的实现
PublicClass::~PublicClass() {
    delete pImpl;
}

// PublicClass 成员函数的实现
void PublicClass::doSomething() {
    pImpl->doInternalWork();
}

// Impl 成员函数的实现
void PublicClass::Impl::doInternalWork() {
    // 这里是内部工作的实现
}

在这个例子中,PublicClass 有一个私有成员 pImpl,它是一个指向 Impl 类实例的指针。Impl 类是 PublicClass 的私有部分,用于封装 PublicClass 的实现细节。外部代码无法直接访问 Impl 类或其成员,因为它们是 PublicClass 的私有部分。这种封装有助于减少编译依赖,因为当 Impl 类的实现改变时,只需要重新编译包含它的 PublicClass 的源文件,而不需要重新编译使用 PublicClass 的其他源文件。

此外,使用 PImpl 惯用法还可以实现二进制兼容性(ABI 兼容性),使得在不改变外部接口的情况下,可以更改库的内部实现并重新编译库,而无需重新编译使用该库的客户端代码。这是库开发者特别关心的一个特性。

4.c++模块(Modules)中,使用类,模块中包含一个导出类和非导出类,非导出类和导出类没有嵌套关系 

在C++模块(Modules)中,你可以定义一个导出类和一个非导出类,即使它们之间没有嵌套关系。导出类将被模块外部的代码所见并使用,而非导出类则仅供模块内部使用。以下是一个示例,展示了如何在同一个模块中定义这样两个类:

模块接口文件(例如 MyModule.ixx

export module MyModule;

// 导出类
export class ExportedClass {
public:
    ExportedClass();
    void publicMethod();

private:
    // 非导出类的指针作为私有成员
    class Impl;
    Impl* pImpl;
};

// 非导出类的前向声明(在模块接口文件中不需要完整定义)
class Impl;

模块实现文件(例如 MyModule.cpp

module MyModule;

#include <iostream>

// 非导出类的定义
class Impl {
public:
    void privateMethod() {
        std::cout << "Private method called" << std::endl;
    }
};

// 导出类的构造函数和成员函数的实现
ExportedClass::ExportedClass() : pImpl(new Impl) {}

void ExportedClass::publicMethod() {
    pImpl->privateMethod();
}

// 注意:这里通常还需要一个析构函数来释放 pImpl 指向的内存
ExportedClass::~ExportedClass() {
    delete pImpl;
}

使用模块的文件(例如 main.cpp

import MyModule;

int main() {
    ExportedClass obj;
    obj.publicMethod(); // 这将调用Impl类的privateMethod方法
    return 0;
}

注意事项

  1. 前向声明:在模块接口文件中,你只需要前向声明非导出类(如上面的 class Impl;),而不需要提供其完整定义。这样做是为了避免在模块接口中暴露过多的实现细节。

  2. 内存管理:在上面的例子中,ExportedClass 负责管理 pImpl 指向的内存。这意味着你需要在 ExportedClass 的析构函数中释放这块内存,以避免内存泄漏。

  3. 封装性:非导出类 Impl 提供了实现细节,而这些细节对模块外部是不可见的。这增强了代码的封装性和可维护性。

  4. 编译隔离:由于非导出类在模块外部不可见,因此它不会增加模块的编译时间,也不会与模块外部的代码产生命名冲突。

通过这种方式,你可以在C++模块中清晰地分离接口和实现,同时保持代码的封装性和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值