当实现在单独的模块单元中时,C++20 模块程序失败
C++20 Module program fails when implementation is in separate module unit
在重构我的项目以与模块一起使用之前,我编写了一个测试项目,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 35\n"; // 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 YELLOW\n";
}
return 44;
}
};
这是使用函数、枚举和整数功能的定义:
// AMemberFunc.hxx
export struct A
{
int MemberFunc()
{
if( num == 35 ) // OK: 'num' is defined in primary interface module 'FuncEnumNum.ixx'
{
std::cout << "num is 35\n"; // 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 YELLOW\n";
}
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
with MyFunc()
在单独的 TU 中定义:
// 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 35\n"; // OK
}
num = MyFunc(); // OK
if( hwColors == HwColors::YELLOW ) OK
{
std::cout << "hwColor is YELLOW\n";
}
return 44;
}
但是当实现在单独的模块中时,VS22 无法找到 'num'、'MyFunc' 和 'HwColor' 的声明。
我对模块的理解是,如果我导入一个接口,就像我在 import FuncEnumNum;
中所做的那样,那么它的所有声明和定义都应该在后续模块中可见。好像不是这样。
关于为什么这在这里不起作用的任何想法?
I do not want to use Module Partitions.
但是...您遇到的问题正是为什么 模块分区存在。这就是他们 for.
无论如何,要记住的重要一点是模块没有改变 C++ 的基本语法规则。它不是“向编译器抛出一堆任意代码并让它解决细节问题”。 C++定义和声明的所有规则仍然存在。
例如,如果声明 int MemberFunc()
出现在 class 定义的 外部 ,它声明了一个 全局函数 ,不是 class 成员函数。即使某处有一个 class 碰巧声明了一个名称为 MemberFunc
的成员函数,C++ 也不会自动将它们关联起来。你声明了一个全局函数,所以这就是你得到的。
如果你想在 class 定义之外定义一个 class 成员函数,你可以。但是你必须为此使用 C++ 的规则:int A::MemberFunc()
.
但这并没有解决问题,因为 C++ 的正常规则仍然存在。具体来说,如果您想在 class 定义之外定义一个 class 成员,则 class 定义必须出现在 之前 行 class 定义。在您假设的 MemberFunc
模块中,A
尚未定义。
记住:模块并不意味着您可以忘记文件之间的关系。编译器看不到名称,只是去寻找恰好实现它的模块。如果您不导入某种 定义 某物的模块,该模块单元将无法使用它。
因此您假设的 MemberFunc
模块需要包含定义结构 A
的任何模块。但是你声明事物的方式,A
是在模块 AStruct
.
中定义的
所以您需要 A::MemberFunc
定义:
- 成为模块的一部分
AStruct
。
- 包括定义 class
A
. 的模块
但是你不能包含你自己的模块。所以如果这个函数定义需要包含一个 class 定义,那么那个 class 定义需要在它自己的模块中定义。但是该模块需要成为 AStruct
模块的一部分,因为它也导出 class 定义。
C++20 有一种模块,它既是模块的一部分,又是模块的可单独包含的组件:“模块分区”。通过将A
的定义放在一个分区中,它可以被模块实现单元导入,并被接口单元导出到模块的接口。
这就是模块分区 for:
///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 35\n"; // 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 YELLOW\n";
}
return 44;
}
///Module interface unit:
export module AStruct;
export import :Def;
My understanding of Modules is, if I import an interface, like I do in import FuncEnumNum;, then all of its declarations and definitions should be visible in subsequent modules.
如果你 export import
它,那么是的。但是“后续模块”是指“导入该模块的模块”。
您认为模块文件共同构建了一个模块,它们共享所有内容。他们没有。每个模块单元对编译器来说都是一个单独的翻译单元。如果一个模块单元,无论是接口、实现还是分区,都没有导入或声明某些东西,那么该模块单元中的代码就不能引用它。即使将组合起来创建最终模块的其他一些模块单元将定义那个东西,为了让您的模块单元引用它,您的模块单元必须导入它。
同样,这就是分区存在的原因:它们允许您创建模块(可导入的代码块)本地模块接口,其他模块可以导入。
如果您想类比预模块 C++ 设计,我们已经有了以下关注点分离。有:
- 外部代码要包含的文件。
- 实现外部代码将直接或间接使用的东西的文件(即:cpp 文件)。
- 定义将内部包含的内容的文件,这些文件在各种实施文件之间共享。
1 和 3 都是头文件,它们的区别仅在于文档、放置这些头文件的位置或某些命名约定。
模块化 C++ 将 1 和 3 识别为不同的概念,因此它为它们创建了不同的概念。 1为主要模块接口单元,2为模块实现单元,3为模块划分单元。请注意,1 可以 export import
定义在 3 中的内容,以便实现单元可以包含特定组件,这些组件也是接口的一部分。
单个文件
我可以根据@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 35\n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOW\n";
}
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 35\n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOW\n";
}
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 35\n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOW\n";
}
return 44;
}
这是一种更传统的方法,区分了声明和定义。模块实现单元提供后者。值得注意的是,全局变量 num
和 hwColors
需要在模块接口单元内部定义。如果您想自己尝试,我有一个代码示例 here。
总结
似乎我们有两个主要的选择来构建带有模块的 C++ 项目:
- 模块分区
- 模块实现
有了分区,我们不需要区分声明和定义,IMO 使代码更易于阅读和维护。如果模块分区单元变得太大,它可以分成几个较小的分区——它们仍然是同名模块的一部分(应用程序的其余部分不需要关心)。
对于实现,我们拥有更传统的 C++ 项目结构,模块接口单元相当于头文件,而实现相当于源文件。
在重构我的项目以与模块一起使用之前,我编写了一个测试项目,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 35\n"; // 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 YELLOW\n";
}
return 44;
}
};
这是使用函数、枚举和整数功能的定义:
// AMemberFunc.hxx
export struct A
{
int MemberFunc()
{
if( num == 35 ) // OK: 'num' is defined in primary interface module 'FuncEnumNum.ixx'
{
std::cout << "num is 35\n"; // 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 YELLOW\n";
}
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
with MyFunc()
在单独的 TU 中定义:
// 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 35\n"; // OK
}
num = MyFunc(); // OK
if( hwColors == HwColors::YELLOW ) OK
{
std::cout << "hwColor is YELLOW\n";
}
return 44;
}
但是当实现在单独的模块中时,VS22 无法找到 'num'、'MyFunc' 和 'HwColor' 的声明。
我对模块的理解是,如果我导入一个接口,就像我在 import FuncEnumNum;
中所做的那样,那么它的所有声明和定义都应该在后续模块中可见。好像不是这样。
关于为什么这在这里不起作用的任何想法?
I do not want to use Module Partitions.
但是...您遇到的问题正是为什么 模块分区存在。这就是他们 for.
无论如何,要记住的重要一点是模块没有改变 C++ 的基本语法规则。它不是“向编译器抛出一堆任意代码并让它解决细节问题”。 C++定义和声明的所有规则仍然存在。
例如,如果声明 int MemberFunc()
出现在 class 定义的 外部 ,它声明了一个 全局函数 ,不是 class 成员函数。即使某处有一个 class 碰巧声明了一个名称为 MemberFunc
的成员函数,C++ 也不会自动将它们关联起来。你声明了一个全局函数,所以这就是你得到的。
如果你想在 class 定义之外定义一个 class 成员函数,你可以。但是你必须为此使用 C++ 的规则:int A::MemberFunc()
.
但这并没有解决问题,因为 C++ 的正常规则仍然存在。具体来说,如果您想在 class 定义之外定义一个 class 成员,则 class 定义必须出现在 之前 行 class 定义。在您假设的 MemberFunc
模块中,A
尚未定义。
记住:模块并不意味着您可以忘记文件之间的关系。编译器看不到名称,只是去寻找恰好实现它的模块。如果您不导入某种 定义 某物的模块,该模块单元将无法使用它。
因此您假设的 MemberFunc
模块需要包含定义结构 A
的任何模块。但是你声明事物的方式,A
是在模块 AStruct
.
所以您需要 A::MemberFunc
定义:
- 成为模块的一部分
AStruct
。 - 包括定义 class
A
. 的模块
但是你不能包含你自己的模块。所以如果这个函数定义需要包含一个 class 定义,那么那个 class 定义需要在它自己的模块中定义。但是该模块需要成为 AStruct
模块的一部分,因为它也导出 class 定义。
C++20 有一种模块,它既是模块的一部分,又是模块的可单独包含的组件:“模块分区”。通过将A
的定义放在一个分区中,它可以被模块实现单元导入,并被接口单元导出到模块的接口。
这就是模块分区 for:
///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 35\n"; // 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 YELLOW\n";
}
return 44;
}
///Module interface unit:
export module AStruct;
export import :Def;
My understanding of Modules is, if I import an interface, like I do in import FuncEnumNum;, then all of its declarations and definitions should be visible in subsequent modules.
如果你 export import
它,那么是的。但是“后续模块”是指“导入该模块的模块”。
您认为模块文件共同构建了一个模块,它们共享所有内容。他们没有。每个模块单元对编译器来说都是一个单独的翻译单元。如果一个模块单元,无论是接口、实现还是分区,都没有导入或声明某些东西,那么该模块单元中的代码就不能引用它。即使将组合起来创建最终模块的其他一些模块单元将定义那个东西,为了让您的模块单元引用它,您的模块单元必须导入它。
同样,这就是分区存在的原因:它们允许您创建模块(可导入的代码块)本地模块接口,其他模块可以导入。
如果您想类比预模块 C++ 设计,我们已经有了以下关注点分离。有:
- 外部代码要包含的文件。
- 实现外部代码将直接或间接使用的东西的文件(即:cpp 文件)。
- 定义将内部包含的内容的文件,这些文件在各种实施文件之间共享。
1 和 3 都是头文件,它们的区别仅在于文档、放置这些头文件的位置或某些命名约定。
模块化 C++ 将 1 和 3 识别为不同的概念,因此它为它们创建了不同的概念。 1为主要模块接口单元,2为模块实现单元,3为模块划分单元。请注意,1 可以 export import
定义在 3 中的内容,以便实现单元可以包含特定组件,这些组件也是接口的一部分。
单个文件
我可以根据@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 35\n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOW\n";
}
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 35\n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOW\n";
}
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 35\n";
}
num = MyFunc();
if( hwColors == HwColors::YELLOW )
{
std::cout << "hwColor is YELLOW\n";
}
return 44;
}
这是一种更传统的方法,区分了声明和定义。模块实现单元提供后者。值得注意的是,全局变量 num
和 hwColors
需要在模块接口单元内部定义。如果您想自己尝试,我有一个代码示例 here。
总结
似乎我们有两个主要的选择来构建带有模块的 C++ 项目:
- 模块分区
- 模块实现
有了分区,我们不需要区分声明和定义,IMO 使代码更易于阅读和维护。如果模块分区单元变得太大,它可以分成几个较小的分区——它们仍然是同名模块的一部分(应用程序的其余部分不需要关心)。
对于实现,我们拥有更传统的 C++ 项目结构,模块接口单元相当于头文件,而实现相当于源文件。