当实现在单独的模块单元中时,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 定义:

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

但是你不能包含你自己的模块。所以如果这个函数定义需要包含一个 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++ 设计,我们已经有了以下关注点分离。有:

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

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

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

总结

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

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

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

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