C++20 当实现在单独的模块单元中时,模块程序失败



在重构我的项目以用于模块之前,我编写了一个测试项目ExImMod,看看我是否可以像模块文档中宣传的那样分离出声明和定义。对于我的项目,我需要将声明和定义保存在单独的翻译单元 (TU) 中,根据模块文档,这也是可能的。我不想使用模块分区。

不幸的是,我的测试ExImMod项目表明它们不能完全分离,至少对于 Visual Studio 2022 (std:c++latest) 编译器 (VS22)。

这是我的主要测试程序:

// ExImModMain.cpp
import FuncEnumNum;
import AStruct;
int main()
{
A a;
a.MemberFunc();
}

A 的成员函数MemberFunc()在此处声明:

// AStruct.ixx
// module; // global fragment moved to AMemberFunc.cppm (Nicol Bolas)
// #include <iostream>
export module AStruct; // primary interface module
export import FuncEnumNum; // export/imports functionalities declared in FuncEnumNum.ixx and defined in MyFunc.cppm
#include "AMemberFunc.hxx" // include header declaration

其中包括"AMemberFunc.hxx"声明和定义:

// AMemberFunc.hxx
export struct A
{
int MemberFunc()
{
if( num == 35 ) // OK: 'num' is defined in primary interface module 'FuncEnumNum.ixx'
{
std::cout << "num is 35n"; // OK: 'cout' is included in global fragment 
}
num = MyFunc(); // OK: 'MyFunc' is declared in primary interface module and defined in 'MyFunc.cppm' module unit
if( hwColors == HwColors::YELLOW ) // OK: 'hwColors' is declared in primary interface module
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}
};

以下是使用函数、枚举和 int 功能的定义:

// AMemberFunc.hxx
export struct A
{
int MemberFunc()
{
if( num == 35 ) // OK: 'num' is defined in primary interface module 'FuncEnumNum.ixx'
{
std::cout << "num is 35n"; // OK: 'cout' is included in global fragment 
}
num = MyFunc(); // OK: 'MyFunc' is declared in primary interface module and defined in 'MyFunc.cppm' module unit
if( hwColors == HwColors::YELLOW ) // OK: 'hwColors' is declared in primary interface module
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}
};

此 TU 声明了以下功能:

//  FuncEnumNum.ixx
export module FuncEnumNum; // module unit
export int num { 35 }; // OK: export and direct init of 'num'
export int MyFunc(); // OK: declaration of 'MyFunc'
export enum class HwColors // OK: declaration of enum
{
YELLOW,
BROWN,
BLUE
};
export HwColors hwColors { HwColors::YELLOW }; // OK: direct init of enum

在单独的 TU 中定义MyFunc()

// MyFunc.cppm
module FuncEnumNum; // module implementation unit
int MyFunc() // OK: definition of function in module unit
{
return 33;
}

这意味着MemberFunc()定义在主界面中,工作正常。但这并不能满足我的项目需求。为了测试这一点,我删除了MemberFunc()的定义;

// AMemberFunc.hxx
export struct A
{
int MemberFunc(); // declares 'MemberFunc'
};

并将其放在单独的 TU 中:

// AMemberFunc.cppm
module;
#include <iostream>
module MemberFunc; // module unit
import AStruct; // (see Nicol Bolas answer)
int MemberFunc()
{
if( num == 35 ) // OK
{
std::cout << "num is 35n"; // OK
}
num = MyFunc(); // OK
if( hwColors == HwColors::YELLOW ) OK
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}

但是当实现在单独的模块中时,VS22 找不到 'num'、'MyFunc' 和 'HwColor' 的声明。

我对模块的理解是,如果我导入一个接口,就像我在import FuncEnumNum;中所做的那样,那么它的所有声明和定义都应该在后续模块中可见。情况似乎并非如此。

关于为什么这在这里不起作用的任何想法?

我不想使用模块分区。

但。。。您遇到的问题正是模块分区存在的原因。这就是他们的目的

无论如何,要记住的重要一点是模块没有改变C++的基本语法规则。它不是"向编译器扔一堆任意代码,让它解决细节"。C++定义和声明的所有规则仍然存在。

例如,如果声明int MemberFunc()出现在类定义之外,则它声明的是全局函数,而不是类成员函数。即使某处有一个类碰巧声明了一个名为MemberFunc的成员函数,C++也不会自动关联它们。你声明了一个全局函数,所以这就是你得到的。

如果要在类定义之外定义类成员函数,则可以。但是你必须使用C++的规则:int A::MemberFunc().

但这并不能解决问题,因为C++的正常规则仍然存在。具体而言,如果要在类定义之外定义类成员,则类定义必须出现在外联类定义之前。在您假设的MemberFunc模块中,尚未定义A

请记住:模块并不意味着您可以忘记文件之间的关系。编译器看不到名称,只是去查找任何模块来实现它。如果不导入定义某些内容的某种模块,则该模块单元不可用

因此,您假设的MemberFunc模块需要包含定义结构A的任何模块。但是你声明事物的方式,A是在模块AStruct中定义的。

因此,您需要A::MemberFunc定义来:

  1. 成为模块AStruct的一部分。
  2. 包括定义类A的模块。

但是不能包含自己的模块。因此,如果此函数定义需要包含类定义,则需要在其自己的模块中定义该类定义。但是该模块需要成为AStruct模块的一部分,因为它也导出了类定义。

C++20 有一种模块,它既是模块的一部分,也是模块中可单独包含的组件:"模块分区"。通过将A的定义放在分区中,可以由模块实现单元导入,并通过接口单元导出到模块的接口。

这就是模块分区的用途

///Module partition
export module AStruct:Def;
export struct A
{
int MemberFunc();
};
/// Module implementation:
module AStruct;
import :Def;
import FuncEnumNum; //We use its interface, but we're not exporting it.
int A::MemberFunc()
{
if( num == 35 ) // OK: 'num' is defined in primary interface module 'FuncEnumNum.ixx'
{
std::cout << "num is 35n"; // OK: 'cout' is included in global fragment 
}
num = MyFunc(); // OK: 'MyFunc' is declared in primary interface module and defined in 'MyFunc.cppm' module unit
if( hwColors == HwColors::YELLOW ) // OK: 'hwColors' is declared in primary interface module
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}
///Module interface unit:
export module AStruct;
export import :Def;

我对模块的理解是,如果我导入一个接口,就像我在导入 FuncEnumNum; 中所做的那样,那么它的所有声明和定义都应该在后续模块中可见。

如果你export import它,那么是的。但"后续模块"是指">导入此模块的模块"。

您认为构建单个模块的模块文件都共享所有内容。他们没有。每个模块单元都是编译器的单独翻译单元。如果模块单元(无论是接口、实现还是分区)不导入或声明某些内容,则该模块单元中的代码无法引用它。即使将组合以创建最终模块的其他模块单元将定义该内容,为了使您的模块单元引用它,您的模块单元必须导入它。

同样,这就是分区存在的原因:它们允许您在模块接口本地创建模块(可导入的代码块),其他模块可以导入该模块。

如果你想类比预模块C++设计,我们已经有以下关注点分离。有:

  1. 外部代码要包含的文件。
  2. 实现外部代码将直接或间接使用的内容的文件(即:CPP 文件)。
  3. 文件,
  4. 用于定义将在内部包含的内容,这些文件在各种实现文件之间共享。

1 和 3 都是头文件,它们仅通过文档、放置这些标头的位置或某些命名约定来区分。

模块化C++将 1 和 3 识别为不同的概念,因此它为它们创建不同的概念。 1 是主模块接口单元,2 是模块实现单元,3 是模块分区单元。请注意,1 可以export import3 中定义的内容,以便实现单元可以包含也是接口一部分的特定组件。

单个文件

我可以在@nicol-Bolas已经很精彩的答案的基础上发展吗?在我看来(是的,这纯粹是基于意见的)模块比头文件有一个好处,我们可以删除代码库中大约 50% 的文件。

用模块分区单元替换头文件不应该是一个通用的概念,而应该只包含.cpp文件(现在使用 C++20 也可以导出模块)。

模块分区接口单元实现单元(或几个!我肯定只有 1 个文件:

// primary module interface unit
export module MyModule;
import <iostream>;
export int num { 35 };
export int MyFunc()
{
return 33;
}
export enum class HwColors
{
YELLOW,
BROWN,
BLUE
};
export HwColors hwColors { HwColors::YELLOW };
export struct A
{
int MemberFunc()
{
if( num == 35 )
{
std::cout << "num is 35n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}
};

多个文件

随着一个文件的增长,可以考虑将代码库划分为"责任区域",并将每个区域放在自己的分区文件中:

// partition
export module MyModule : FuncEnumNum;
export int num { 35 };
export int MyFunc()
{
return 33;
}
export enum class HwColors
{
YELLOW,
BROWN,
BLUE
};

export HwColors hwColors { HwColors::YELLOW };
// partition
export module MyModule : AStruct;
import :FuncEnumNum;
export struct A
{
int MemberFunc()
{
if( num == 35 )
{
std::cout << "num is 35n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}
};
// primary interface unit
export module MyModule;
export import :FuncEnumNum;
export import :AStruct;

大型库的文档

不幸的是,头文件具有一个重要的功能,因为它们是没有自己的wiki设置的项目的绝佳文档来源。

如果源代码在没有正式文档页面的情况下分发,那么 @nicol-Bolas 的答案是我见过的最好的答案。在这种情况下,我会在主模块接口单元中放置注释:

// primary module interface unit
export module MyModule;
/*
* This function does this and that.
*/
export int MyFunc();
module MyModule;
int MyFunc()
{
return 33;
}

但是该文档可以放置在任何地方,并与doxygen或其他此类工具一起使用。我们将不得不拭目以待,看看未来几年软件分发的最佳实践如何发展。

没有模块分区

如果您的编译器对模块分区有未完成的支持,或者您在应用它们时犹豫不决,则可以轻松编写源代码而无需:

// primary module interface unit
export module MyModule;
export int num { 35 };
export int MyFunc();
export enum class HwColors
{
YELLOW,
BROWN,
BLUE
};
export HwColors hwColors { HwColors::YELLOW };
export struct A
{
int MemberFunc();
};
// module implementation unit
module MyModule;
import <iostream>;
int MyFunc()
{
return 33;
}
int A::MemberFunc()
{
if( num == 35 )
{
std::cout << "num is 35n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOWn";
}
return 44;
}

这是一种比较传统的方法,区分声明定义。模块实现单元提供后者。值得注意的是,全局变量numhwColors需要在模块接口单元内定义。如果您想自己尝试,我这里有一个代码示例。

总结

似乎我们有 2 个主要选择来构建带有模块的C++项目:

  1. 模块分区
  2. 模块实现

对于分区,我们不需要区分声明定义,IMO 使代码更易于阅读和维护。如果模块分区单元变得太大,则可以将其分成几个较小的分区 - 它们仍将是同一命名模块的一部分(应用程序的其余部分不需要关心)。

在实现中,我们有更传统的C++项目结构,模块接口单元与头文件相当,实现作为源文件。

最新更新