Reputation: 546
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?
Upvotes: 3
Views: 3303
Reputation: 1608
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;
}
};
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;
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.
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.
It seems we have 2 main choices for structuring C++ projects with modules:
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.
Upvotes: 5
Reputation: 474276
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:
AStruct
.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:
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.
Upvotes: 11