Nicol Bolas
Nicol Bolas

Reputation: 473926

The orthogonality of module interface/implementation units and partitions

The C++20 standard appears to define two classifications of module units: interface/implementation units, and whether a module unit is a partition or not. These two classifications appear to be orthogonal: you can have an implementation unit that is a partition, an interface unit which isn't a partition, and so forth.

The interface/implementation axis of classification seems to be about what you can import and what you can't. But if that's true, what's the point of an implementation unit that is a named partition? Couldn't you just make that implementation unit not be a partition?

Are these two concepts truly orthogonal, or are they somewhat interdependent? And if it is the latter, to what degree are they dependent on one another?

Upvotes: 14

Views: 1122

Answers (3)

Tom Huntington
Tom Huntington

Reputation: 3425

Edit

You don't need an internal partition to import the Impl struct as a simple partition will do. You can replace m.cpp with m.ixx.


Here's an example using pimpl of a Pure Interface Unit with a Partitioned implementation unit.

To break the dependency of Impl on the files that import Base we need Impl to only be in the implementation units and not the interface. But what if you want to separate the Impl struct across different TUs.

//m.ixx - Pure Interface unit
export module m;

export struct Base {
    virtual ~Base() = default;
    virtual void LargeFunction1() = 0;
    virtual void LargeFunction2() = 0;
    static std::unique_ptr<Base> Create();
};

// m.cpp - Partitioned Implementation unit. msvc /internalPartition flag
module m:impl_def;
import m;
import std;

struct Impl : Base {
    void LargeFunction1() override;
    void LargeFunction2() override;
};

std::unique_ptr<Base> Base::Create() {
    return std::make_unique<Impl>();
}
// m1.cpp - Pure Implementation unit
module m;
import :impl_def;
import std;

void Impl::LargeFunction1() {
    std::cout << "hello\n";
}
//m2.cpp - Pure Implementation unit
module m;
import :impl_def;
import std;

void Impl::LargeFunction2() {
    std::cout << "Other hello\n";
}

Pure was not a good name as it's not mutually exclusive with a partition. Perhaps global might have been better?

Upvotes: 0

Nicol Bolas
Nicol Bolas

Reputation: 473926

These two axes of module unit classification are orthogonal, in the sense that a module can independently be a part of any combination of these classifications. However, the standard defines a number of specialized rules about each of the 4 kinds of classifications, which makes the use of each something more than just the classification definitions would indicate.

Before we look at the combinations of these, we first need to define what is being classified.

Interface unit vs. implementation unit

An interface unit is not a thing you can import. Well, you can, but that's not the definition of "interface unit". A module unit is an interface unit of the module M because it is a component of the interface of module M. This means that, if someone imports the module M, the build system will need to build all interface units of the module M. An implementation unit of the module M does not need to be built before someone can import M.

This is all the interface/implementation classification means (though it's not all that it does, but we'll get to that). Implementation units are conceptually parts of the module M, but they are not part of the interface of it.

It is important to note what it means to be "part of the module M". If an entity is declared within the purview of M, it is part of M. So if you want to declare it again (because you're defining it, let's say), that second declaration must also be in the purview of M ([basic.link]/10).

This is the point of implementation units of all kinds: to be within the purview of M without contributing to its interface.

Partition vs. Pure

There is no terminology in the standard for a module unit that is not a partition, so I will refer to such module units as "pure".

A module unit which is a partition X of the module M can be imported via partition import syntax: import :X. This can only be done by a module unit that is part of M. Pure module units cannot be imported in such a fashion.

So the partition vs. pure classification is about whether a module unit within a module can import some module unit within the same module through a special syntax.

It is also important to note what it means to import something. Importing a thing is done on a translation unit basis. To import a non-partition module means to import all of the interface module unit TUs of that module. To import a module partition is to only import that partition unit.

However, export only matters for declarations being imported by code outside of the module that declared them. So if some module unit of M imports a partition unit of M, it will see all of the declarations in the purview of that partition unit whether they are exported or not ([basic.scope.namespace]/2) .

Now, let us examine all of the special-case rules C++ defines for each of the four combinations. To whit:

Pure Interface Unit

This combination has so many special rules attached to it that the standard gives it a name: the primary interface unit for a module M.

If we just look at the rules above, a primary interface unit of M is a component of the interface of M. And since it is pure, a primary interface unit of M cannot be imported through partition syntax.

But then the standard sets up a bunch more rules on top of that:

  1. For any module M, there shall be exactly and only one primary interface unit for M ([module.unit]/2).

  2. All partition interface units of M must be export imported (directly or indirectly) by the primary interface unit of M ([module.unit]/3).

  3. If there are no other implementation or interface units of M, then this file may have a private module fragment, used for putting the non-exported stuff for M in a single file ([module.private.frag]).

In short: if the build system ever needs to build the module M, what that really means is that it needs to build this file (and anything it imports). This file is the importation root which defines what import M; will expose.

Interface partition unit

Such module units are a component of the interface of module M, and therefore must be compiled to generate the module M. But that was handled because primary interface unit has to include all of these. They can also be included... which we know, because the primary interface unit had to include them.

So there aren't any special rules for this one that haven't been covered elsewhere.

The meaning of an interface partition unit is just to be a tool for separating large module interfaces into multiple files.

Pure implementation unit

As implementation units, they do not contribute to a module's interface. And as pure module units, they cannot be imported as partitions. This means everything that happens within them stays within them (as far as importing anything is concerned).

But they also have a couple of special rules:

  1. All pure implementation units of M implicitly import M; ([module.unit]/8).

  2. They cannot explicitly import M; ([module.import]/9).

If the purpose of an implementation unit is to be able to define the interface features of a module, then these rules make some sense. As previously stated, only module units of M can define declarations made as part of M's interface. So these are the files where most of the definitions will go.

So they may as well implicitly include the interface of M as a convenience.

Partition implementation unit

This is a module unit which is not part of the interface of its module. But since it is a partition, it can be imported by other module units of M.

This sounds contradictory, right up until you get to this special case rule:

  1. Module units cannot export import a partition implementation unit ([module.import]/8).

So even if an interface unit imports an implementation partition, it cannot export that partition. Implementation units also cannot export anything defined within it (you're not allowed to redeclare un-exported things as exported later).

But recall that export only matters for importing non-partitions (ie: other modules). Since partitions can only be imported by members of their own modules, and all declarations in an imported partition will be made available to the importing code, what we have are module units that contain declarations that are private to the implementation of a module, but need to be accessible by multiple module implementation units.

This is particularly important as module names are global, while partition names are local to a module. By putting internal shared code into an implementation partition, you don't pollute the global space of module names with implementation details of your module.

Upvotes: 12

Serikov
Serikov

Reputation: 1209

The C++20 standard appears to define two classifications of module units: interface/implementation units, and whether a module unit is a partition or not.

There is another important class of module units (and the most important one) - primary module interface.

Named module has to contain exactly one primary module interface and optionally can contain multiple implementation units, multiple interface partitions and multiple implementation partitions.

The interface/implementation axis of classification seems to be about what you can import and what you can't.

No. It is about what can contribute to named module interface and what can't. Module interface unit can export something and so can contribute to module interface. Implementation units can't export anything (so can't be exported themselves) and therefore only contribute to the implementation of the module.

The interface of the named module is defined by the primary module interface unit. If named module contains other interface units (interface partitions) then they should be exported directly or indirectly (transitively) from the primary module interface.

But if that's true, what's the point of an implementation unit that is a named partition? Couldn't you just make that implementation unit not be a partition?

First let's consider how module partitions differs from "ordinary" module implementation units.

Module implementation units that are not partitions automatically (implicitly) import corresponding module interface. When we write ordinary ".cpp/.hpp" files most of the time we include corresponding header file from source file as the first line of it. That's it, module implementation units are analogue of that ordinary source files.

Why do we want to have partitions?

As it is impossible to forward declare a class from another module it is sometimes necessary to unite what otherwise could be separate but related modules into one compound module. When doing so it can be unwieldy to write all interface of the compound module in one file. In C++20 it is possible to use module interface partitions to separate module interface into multiple files. Similarly it is possible to divide implementation between files using "implementation module partitions".

It is possible to import one module partition into the other with import :partition-name; syntax so it is possible to

  • declare entity in the partition A,
  • import partition A into the partition B to use this entity
  • define that entity in the partition C.

It is like header files and source files but inside single module.

Considering that private module fragment can appear only when named module consist of q single module unit (primary module interface unit) we can say that there are three ways to structure named module:

  1. Single file module (primary module interface with optional private fragment inside of it).

  2. Primary interface unit + "unnamed" implementation unit(s).

This is "header file + source file" alternative."Unnamed" implementation units implicitly import module interface which is nice.

One use case is that separation of implementation and interface can limit recompilation of the dependent modules when changes are limited to the implementation files if used with build systems relying on file timestamps. Another use case is to have multiple implementations of one common primary module interface that can be selected build-time by the build system script. For example distinct module implementation for particular OS.

  1. Library as a module: primary interface unit + multiple interface partitions + multiple implementation partitions.

It is analogue for library with multiple public headers and multiple private source files.

Primary interface partition defines API surface and serve as single entry point for the library (like one "include-all.hpp"). All other interface partitions should be directly or indirectly exported out of it.

Partitions does not automatically import module interface. Partitions could explicitly import either individual sibling partitions separately or module as a whole. This is an analogue of the inclusion of the header files from the inside of the library.

This module structure could be used for large modules with interdependent types that can't be separated into submodules.

When using this variant of module structure it is actually possible to additionally use "unnamed" module implementation unit, but IMO it brings nothing new to the table in this case.

Upvotes: 3

Related Questions