原文地址:devblogs.microsoft.com/cppblog/a-t…
原文作者:devblogs.microsoft.com/cppblog/aut…
发布时间:2020年10月29日
C++模块支持已经在Visual Studio中出现了!如果你想尝试一下,请抓紧时间下载最新的Visual Studio预览版。如果你想尝试一下,请抓紧时间下载最新的Visual Studio预览版。C++模块可以帮助你将代码分门别类,加快构建时间,而且它们可以与你现有的代码并排无缝工作。
此预览仅支持MSBuild项目的IDE中的C++模块。虽然任何构建系统都支持MSVC工具集,但Visual Studio的IDE对CMake的支持还不支持C++模块。一旦支持了,我们会告诉你。一如既往,请试用它,如果你有任何反馈,请告诉我们。
模块基础
C++模块允许你密切控制提供给使用它们的翻译单元的内容。与头文件不同,它们不会泄露宏定义或私有的实现细节(不需要可笑的前缀)。此外,与头文件不同,它们只需构建一次,然后可以在你的项目中多次消耗,从而减少构建开销。
C++20引入了新的关键字来定义和消耗模块,Visual Studio使用新的文件类型".ixx "来定义模块的接口。请继续阅读以了解详情。
在Visual Studio中开始使用模块
如果你在最新的预览版中创建了一个全新的项目,你不需要做任何事情。但是,在你在现有项目中添加或消耗模块之前,你需要确保你使用的是最新的C++语言标准。
要做到这一点,请将C++语言标准设置为 "预览/std:c++latest"。如果你的解决方案中有多个项目,记得对所有项目都这样做。
这就是了! 你可以在Visual Studio中使用C++模块了。
创建模块
要在项目中添加一个模块,您需要创建一个模块接口。这些都是普通的C++源文件,扩展名为".xxx",它们可以包含头文件,导入其他模块,并包含你的模块的导出定义。它们可以包含头文件,导入其他模块,并将包含你的模块的导出定义。你可以在一个项目中添加任意数量的模块。
下面是在Solution Explorer中的样子。在这个例子中,fib和printer项目都定义了C++模块。
注意:虽然这个例子显示了".ixx "文件中的所有模块接口,但任何C++源文件都可以被视为模块接口。要做到这一点,将源文件上的 "Compile As "属性设置为 "Compile As Module"。Compile As "属性可以在任何源文件属性页面的 "Advanced "标签上找到。
导出模块
那么,模块接口中到底有哪些内容呢?下面的例子定义了一个名为DefaultPrinter的简单模块,并导出了一个单一结构。
module; //begins global module fragment
#include <iostream>
export module DefaultPrinter;
export struct DefaultPrinter
{
void print_element(int e)
{
std::cout << e << " ";
}
void print_separator()
{
std::cout << ", ";
}
void print_eol()
{
std::cout << '\n';
}
};
要把这个例子分解一下,你可以在第1、5和7行看到新的导出语法。第1行指定这是一个模块接口。第5行定义并导出模块本身,第7行导出一个struct。每个模块可以导出很多项目,比如结构体、类、函数和模板。
模块接口可以包含头文件和导入其他模块。当它们被导入时,除非你明确地导入它们,否则它们不会从这些包含的头文件或模块中泄露任何细节。这种隔离可以帮助避免命名碰撞和泄漏实现细节。你也可以在模块接口中安全地定义宏和使用命名空间。它们不会像传统的头文件那样泄露。
要在模块接口中包#include文件,确保你把它们放在module;和export module mymodule;之间的全局模块片段中。
这个例子把实现放在模块的接口中,但这是可选的。如果你回看之前的解决方案探索器,可以看到fibgen.ixx接口在fibgen.cpp中有相应的实现。
它的接口是这样的。
export module FibGenerator;
export fib gen_fib(int start, int &len);
有相应的实现。
module FibGenerator;
fib gen_fib(int start, int &len)
{
//...
}
在这里,接口定义了模块名称,并输出gen_fib。相应的实现使用module关键字来定义该实现属于哪个模块,这样所有的东西都可以在构建时自动组合成一个内聚的单元。
消耗模块
要消费模块,请使用新的import关键字。
module;
#include <ranges>
#include <concepts>
import DefaultPrinter;
struct DefaultFormatter
{
template<is_series S, is_printer T>
void format(T t, S s)
{
while (!s.done())
{
t.print_element(s.next());
t.print_separator();
}
t.print_eol();
}
};
从模块接口导出的所有项目都可以使用。这个例子使用了第一个例子中的DefaultPrinter模块,在第5行导入了它。
您的代码可以自动消耗同一项目中的模块或任何引用的模块(使用项目到项目的静态库项目引用)。
消耗其他模块的模块
您也可以从其他模块接口导入模块。下面是一个在上面DefaultPrinter模块基础上扩展的例子。
module;
#include <iostream>
import DefaultPrinter;
export module TabbedPrinter;
export struct TabbedPrinter : DefaultPrinter
{
void print_separator()
{
std::cout << "\t";
}
};
这个例子导入了上面的DefaultPrinter模块,并覆盖了它的print_separator函数。其他代码现在可以导入这个TabbedPrinter,而不需要担心DefaultPrinter的细节。Visual Studio将确保一切都以正确的顺序构建。
外部模块
也可以引用存在于磁盘上的模块,而不是解决方案中属于另一个项目的模块。但是,这里需要注意,因为模块是编译过的二进制文件,你必须确保它们与你的项目构建方式兼容。您必须确保它们与您构建项目的方式兼容。
你可以通过编辑Additional Module Dependencies属性来告诉Visual Studio在磁盘上寻找模块。
智能系统和模块
您所熟知和喜爱的所有IntelliSense功能也能与模块一起使用。诸如代码完成、参数帮助、查找所有引用、转到定义和声明、重命名等功能,都能以您使用模块时期望的方式在解决方案中使用。
这里你可以看到Find All References和Peek Definition与我们上面的TabbedPrinter模块一起工作。例如,它可以显示从DefaultPrinter模块导出的DefaultPrinter结构的所有引用,并显示其定义。
查找所有引用
Peek定义
您也可以在导入模块的任何地方转到或偷看模块本身的定义。
见行动中的模块
要查看所有这些操作,请查看我们在CppCon 2020的模块演示。如果你有兴趣的话,还有很多其他的Visual Studio和C++20最新特性的演示。
头部单元
头单元是一个标准的C++咒语,用于调用元数据(IFC文件)的生成--对于行为良好的头文件,尤其是标准库头文件--类似于为模块生成的元数据,目的是加快整体构建时间,如果做得很明智的话。然而,与模块不同的是,头单元并不像模块那样真正提供隔离:宏定义和其他预处理器状态仍然会泄露给头单元的消费者。你可以通过import "header.h";或import <header>;语法来使用头单元。在Visual Studio中,头单元的元数据是由构建系统自动生成的。头文件(及其包含)中所有声明的项目和合理的定义都会向消费者提供,就像#include文件一样。就像在模块消费的情况下一样,宏定义和其他在导入头单元的代码中活跃的预处理器状态不会以任何方式影响导入的头单元。然而,与模块不同的是,当你导入一个头单元时,任何宏定义都可以在你的代码中使用。头单元主要是一种过渡机制,而不是模块的替代品。如果你有机会考虑命名模块和头单元,我们鼓励你投入精力去设计合适的模块。我们将在未来的博客中深入介绍头单元,特别是它们在将现有代码库迁移到模块用途中的使用。
对头单元的全面IDE和工具集支持即将到来。你可以在GitHub上跟踪微软STL的头单元支持状态。
反馈
如果你对用自己的代码尝试C++模块感兴趣,我劝你抓紧下载最新的Visual Studio预览版。请试用它,如果你有任何问题或反馈,请告诉我们。如果你发现任何问题或有建议,最好的方法是报告问题。
通过www.DeepL.com/Translator(免费版)翻译