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

2022-05-17 00:00:00 c++ c++20 c++-modules visual-studio-2022

在重构项目以与模块一起使用之前,我编写了一个测试项目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
"; // 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
";
    }

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

    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 35
"; // OK
  }

  num = MyFunc(); // OK

  if( hwColors == HwColors::YELLOW ) OK
  {
    std::cout << "hwColor is YELLOW
";
  }

  return 44;
}

但当实现位于单独的模块中时,VS22找不到‘Num’、‘MyFunc’和‘HwColor’的声明。

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

有什么想法说明为什么这在这里不起作用?


解决方案

单个文件

我可以继续@Nicol-Bolas的精彩回答吗?在我看来(是的,这纯粹是基于意见)模块比头文件有一个好处,我们可以在代码库中删除大约50%的文件。

不应该将头文件替换为模块分区单元,而应该只拥有.cpp文件(现在使用C++20也可以导出模块)。

模块分区和一个接口单元和一个实现单元(或几个!)有一些维护开销。我肯定只有一个文件:

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

        num = MyFunc();

        if( hwColors == HwColors::YELLOW )
        {
            std::cout << "hwColor is YELLOW
";
        }

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

        num = MyFunc();

        if( hwColors == HwColors::YELLOW )
        {
            std::cout << "hwColor is YELLOW
";
        }

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

    num = MyFunc();

    if( hwColors == HwColors::YELLOW )
    {
        std::cout << "hwColor is YELLOW
";
    }

    return 44;
}

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

摘要

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

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

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

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

相关文章