3

Before re-factoring my project for use with Modules I wrote a test project, ExImMod, to see if I could separate out declarations and definitions as advertised in the Modules documentation. For my project, I need to keep the declarations and definitions in separate translation units (TU), which is also possible according to the Modules documentation. I do not want to use Module Partitions.

Unfortunately, my test ExImMod project indicates that they cannot be fully separated, at least for the Visual Studio 2022 (std:c++latest) compiler (VS22).

Here is my main test program:

// ExImModMain.cpp
import FuncEnumNum;
import AStruct;

int main()
{
  A a;
  a.MemberFunc();
}

A's member function, MemberFunc(), is declared here:

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

which includes the `AMemberFunc.hxx' declaration and definition:

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

Here is the definition that uses the function, enum and int functionalities:

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

This TU declares these functionalities:

//  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() defined in a separate TU:

// MyFunc.cppm
module FuncEnumNum; // module implementation unit

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

This means MemberFunc() definition is in the primary interface, which works fine. But this does not do what I need for my project. To test that, I remove the definition of MemberFunc();

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

and put it in a separate 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;
}

But VS22 cannot find the declarations for 'num', 'MyFunc' and 'HwColor' when the implementation is in the separate module.

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. This does not seem to be the case.

Any ideas as to why this doesn't work here?

1
  • 1
    Answers should not be integrated into questions. You can post them as answers; it's OK to answer your own question. Commented Nov 27, 2021 at 22:14

2 Answers 2

11

I do not want to use Module Partitions.

But... the problem you're having is exactly why module partitions exist. This is what they're for.

In any case, the important thing to remember is that modules did not change C++'s basic syntax rules. It's not "throw a bunch of arbitrary code at the compiler and let it work out the details". All the rules of C++'s definitions and declarations still exist.

For example, if the declaration int MemberFunc() appears outside of a class definition, it declares a global function, not a class member function. Even if there is a class somewhere that happens to declare a member function with the name MemberFunc, C++ doesn't automatically associate them. You declared a global function, so that's what you get.

If you want to define a class member function outside of the class definition, you can. But you have to use C++'s rules for that: int A::MemberFunc().

But that doesn't solve the problem because again, C++'s normal rules still exist. Specifically, if you want to define a class member outside of the class definition, the class definition has to appear before the out-of-line class definition. And in your hypothetical MemberFunc module, A has not yet been defined.

Remember: modules don't mean you get to forget about the relationships between files. The compiler doesn't see a name and just go find whatever module happens to implement it. If you don't import a module of some sort that defines something, it is unavailable to that module unit.

So your hypothetical MemberFunc module needs to include whatever module defines the struct A. But the way you declared things, A is defined in the module AStruct.

So you need your A::MemberFunc definition to:

  1. Be part of the module AStruct.
  2. Include the module defining the class A.

But you can't include your own module. So if this function definition needs to include a class definition, then that class definition needs to be defined in its own module. But that module needs to be part of the AStruct module, since it exports the class definition as well.

C++20 has a kind of module that is both a part of a module and a separately-includable component of it: "module partition". By putting A's definition in a partition, it can be imported by module implementation units, and exported to the module's interface by interface units.

This is what module partitions are 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.

If you export import it, then yes. But "subsequent modules" means "modules which import this module".

You're thinking that the modules files which, in aggregate, build a single module all share everything. They don't. Each module unit is a separate translation unit to the compiler. If a module unit, whether interface, implementation, or partition, does not import or declare something, then the code in that module unit cannot reference it. Even if some other module unit that will be combined to create the final module will define that thing, in order for your module unit to reference it, your module unit has to import it.

Again, this is why partitions exist: they allow you to create modules (importable chunks of code) local to a module interface which other modules can import.

If you want an analogy to pre-module C++ design, we already have the following separation of concerns. There are:

  1. files that external code is meant to include.
  2. files that implement the things that the external code will directly or indirectly use (ie: cpp files).
  3. files which define things that will be internally included, which are shared among the various implementation files.

Both 1 and 3 are header files, and they're only differentiated by documentation or by where you put those headers or by some naming convention.

Modular C++ recognizes 1 and 3 as distinct concepts, so it creates distinct concepts for them. 1 is the primary module interface unit, 2 are module implementation units, and 3 are module partition units. Note that 1 can export import stuff defined in 3 so that implementation units can include specific components that are also part of the interface.

12
  • "But... the problem you're having is exactly why module partitions exist. This is what they're for." Not so. I contend there is no reason for the partition capability, not even in the standard. But that is a larger, more theoretical discussion. Yes, int A::MemberFunc() needs to be qualified to be a member of A struct. And, "If a module unit, whether interface, implementation, or partition, does not import or declare something, then the code in that module unit cannot reference it." Very helpful for my thinking. I added imports to module MemberFunc and it compiled and ran. Commented Nov 27, 2021 at 21:21
  • Please edit out your partition discussion in your Answer so I can make your Answer the Answer. Commented Nov 27, 2021 at 21:58
  • 1
    @rtischer8277: To me, the main thing I want to communicate is that your primary problem is your belief that "there is no reason for the partition capability, not even in the standard". The intent in my answer is to undermine this subjective belief, so that others are not swayed by it. Module partitions have a purpose, and your code exemplifies that purpose. Your solution is to have a new module, just to avoid using the feature. If you want to provide that as an answer and accept it, feel free, but I consider the module partition part of this answer to be critical to the goal of this answer. Commented Nov 27, 2021 at 22:13
  • 1
    @rtischer8277: "I see no problem with defining a new module." There is a problem: a namespace problem. Module names are global; if your module is built by including a bunch of other modules, you've created a bunch of other names that nobody else can use for their modules. And you can't include any project that accidentally uses that module name, even if its an internal "sub-module" that the outside world doesn't need to know about. Partition names are local to a module; there can be no possibility of collisions. Commented Nov 27, 2021 at 23:23
  • 1
    @rtischer8277 Modules remove a lot of use cases where namespaces are required, that is true. If you have code inside a namespace inside a module, then you first import the module then using the namespace. But having namespaces is far better than all prefixed functions in global namespace, like in C. Besides, namespaces in C++ can still be used with modules, e.g. if a module contains several (optionally nested) namespaces. Asides from this, module partitions are here for a reason, and they should be applied thusly.
    – alexpanter
    Commented Nov 30, 2021 at 22:24
5

Single file

May I evolve on the already brilliant answer by @nicol-Bolas? In my opinion (and yes, this is purely opinion-based) modules have a benefit over header files that we can remove about 50% of files in code bases.

It should not be a general notion to replace header files with a module partition unit, but instead to only have the .cpp file (which with C++20 now exports a module as well).

There is a bit of maintenance overhead with module partitions and an interface unit and an implementation unit (or several!). I would definitely only have 1 file:

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

Multiple files

As that one file grows, it could be considered to divide the code base into "responsibility areas" and place each of these in its own partition file:

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

Documentation for larger libraries

Unfortunately, header files carry an important function in that they are an excellent source of documentation for projects that don't have their own wiki set up.

If source code is distributed without a formal documentation page, then the answer by @nicol-Bolas is the best that I have yet seen. In that case I would place comments in the primary module interface unit:

// primary module interface unit
export module MyModule;

/*
 * This function does this and that.
 */
export int MyFunc();
module MyModule;

int MyFunc()
{
    return 33;
}

But that documentation could be placed anywhere, and used together with doxygen or other such tool. We will have to wait and see how best practices evolve for software distribution over the next few years.

Without module partitions

In case your compiler has unfinished support for module partitions, or you are otherwise hesitant in applying them, the source code can easily be written without:

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

This is a more traditional approach with distinction between declaration and definition. The module implementation unit provides the latter. Notably, the global variables num and hwColors need to be defined inside the module interface unit. I have an example of the code here if you wish to try it yourself.

Summary

It seems we have 2 main choices for structuring C++ projects with modules:

  1. module partitions
  2. module implementations

With partitions we need not have a distinction between declaration and definition, which IMO makes code easier to read and maintain. If a module partition unit grows too big it can be separated into several smaller partitions - they will still be part of the same named module (rest of application will not need to care).

With implementations we have the more traditional C++ project structure with the module interface unit being comparable to a header file, and the implementation(s) as the source file.

3
  • Nicol Bolas answer may be brilliant, but it only answers 2/3s of my original posting. It corrected a qualification bug and a module source code thinking bug I was having. But it added unnecessary partitions. I finished answering my own question with additional module code which I edited into the original posting. In case you missed my note, the code now runs fine. Without partitions. Either you or Nicol Bolas is welcome to write up an Answer and I will check it, but only if it reflects the revised code above without discussion of partitions. Commented Nov 30, 2021 at 21:22
  • @rtischer8277 If you really want to avoid partitions, then that is pretty simple to do. I will add a section to my post, in case someone else might be curious.
    – alexpanter
    Commented Nov 30, 2021 at 21:34
  • 2
    Just to add to the answer, you can also have everything in one file, but still have a distinction between interface and implementation, by using the module :private fragment.
    – dgellow
    Commented Dec 22, 2021 at 13:53

Not the answer you're looking for? Browse other questions tagged or ask your own question.