Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 290
Nicol Bolas Avatar asked Jan 26 '26 23:01

Nicol Bolas


1 Answers

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.

like image 158
Nicol Bolas Avatar answered Jan 29 '26 12:01

Nicol Bolas



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!