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

Posted

技术标签:

【中文标题】当实现在单独的模块单元中时,C ++ 20模块程序失败【英文标题】:C++20 Module program fails when implementation is in separate module unit 【发布时间】:2022-01-05 06:07:53 【问题描述】:

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

不幸的是,我的测试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;
  
;

这是使用函数、枚举和 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 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

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; 中所做的那样,那么它的所有声明和定义都应该在后续模块中可见。好像不是这样的。

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

【问题讨论】:

答案不应整合到问题中。您可以将它们作为答案发布;可以回答你自己的问题。 【参考方案1】:

我不想使用模块分区。

但是...您遇到的问题正是 为什么 存在模块分区。这就是他们的目的

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

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

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

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

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

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

所以你需要你的A::MemberFunc 定义来:

    加入模块AStruct。 包括定义类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 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;

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

如果你 export import 它,那么是的。但是“后续模块”是指“导入这个模块的模块”。

您认为,总体上构建单个模块的模块文件都共享所有内容。他们没有。每个模块单元对于编译器来说都是一个独立的翻译单元。如果一个模块单元,无论是接口、实现还是分区,都没有导入或声明某些东西,那么该模块单元中的代码就不能引用它。即使将组合以创建最终模块的其他模块单元将定义该事物,为了让您的模块单元引用它,您的模块单元也必须导入它。

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

如果您想与模块前的 C++ 设计进行类比,我们已经有了以下关注点分离。有:

    外部代码要包含的文件。 实现外部代码将直接或间接使用的东西的文件(即:cpp 文件)。 定义将在内部包含的内容的文件,这些文件在各种实现文件之间共享。

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

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

【讨论】:

“但是……您遇到的问题正是模块分区存在的原因。这就是它们的用途。”不是这样。我认为分区功能没有理由,即使在标准中也是如此。但这是一个更大、更理论的讨论。是的,int A::MemberFunc() 需要有资格成为 A 结构的成员。并且,“如果一个模块单元,无论是接口、实现还是分区,都没有导入或声明某些东西,那么该模块单元中的代码就不能引用它。”对我的思考很有帮助。我将导入添加到 module MemberFunc 并编译并运行。 请在您的答案中编辑您的分区讨论,以便我可以将您的答案作为答案。 @rtischer8277:对我来说,我想传达的主要问题是您的主要问题是您认为“没有理由进行分区功能,甚至在标准中也没有我>”。我回答的意图是破坏这种主观信念,以免其他人被它所左右。模块分区是有目的的,你的代码就是这个目的的例证。您的解决方案是拥有一个新模块,只是为了避免使用该功能。如果您想提供它作为答案并接受它,请随意,但我认为此答案的模块分区部分对于此答案的目标至关重要。 @rtischer8277:“我认为定义新模块没有问题。”有一个问题:命名空间问题。模块名称是global;如果您的模块是通过包含一堆其他模块来构建的,那么您已经创建了一堆其他人无法为其模块使用的其他名称。并且您不能包含任何意外使用该模块 name 的项目,即使它是外部世界不需要知道的内部“子模块”。分区名称是模块的本地;不可能发生碰撞。 @rtischer8277 模块删除了很多需要命名空间的用例,这是真的。如果您在模块内的命名空间中有代码,那么您首先导入模块然后using 命名空间。但是拥有命名空间比全局命名空间中的所有前缀函数要好得多,就像在 C 中一样。此外,C++ 中的命名空间仍然可以与模块一起使用,例如如果一个模块包含多个(可选嵌套)命名空间。除此之外,模块分区在这里是有原因的,它们应该被如此应用。【参考方案2】:

单个文件

我可以在@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 的回答是我所见过的最好的。在这种情况下,我会将 cmets 放在主模块接口单元中:

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

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

总结

对于使用模块构建 C++ 项目,我们似乎有 2 个主要选择:

    模块分区 模块实现

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

对于实现,我们有更传统的 C++ 项目结构,模块接口单元类似于头文件,而实现作为源文件。

【讨论】:

Nicol Bolas 的回答可能很棒,但它只回答了我原始帖子的 2/3。它纠正了我遇到的一个资格错误和一个模块源代码思考错误。但它添加了不必要的分区。我用我在原始帖子中编辑的附加模块代码完成了我自己的问题的回答。万一你错过了我的笔记,代码现在运行良好。没有分区。欢迎您或 Nicol Bolas 写一个答案,我会检查它,但前提是它反映了上面的修订代码而不讨论分区。 @rtischer8277 如果你真的想避免分区,那么这很简单。我会在我的帖子中添加一个部分,以防其他人好奇。 只是为了补充答案,您也可以将所有内容放在一个文件中,但仍然有 interfaceimplementation 之间的区别,通过使用module :private 片段。

以上是关于当实现在单独的模块单元中时,C ++ 20模块程序失败的主要内容,如果未能解决你的问题,请参考以下文章

知识点5:集成测试

如何将 ZF2 单元/应用程序模块测试合并到单个调用中?

使用来自单独的表视图控制器的选定单元格填充文本字段

用户和单独模块的基于令牌的身份验证

当行悬停在 ag-grid 中时,在单元格中呈现附加信息

十四什么是集成测试?