It’s been a while, but here we are with Part 2 of Understanding C++ Modules!

In the previous post we covered the very basics of C++ modules, but there are still important things that need to be discussed.

(The posting of this article on April 1st is purely an accident in being to lazy to get it out sooner, and not wanting to delay it further. There will be no April Fool’s goofs in this page.)

This post will focus on what may be the most cursed keyword in the C++ language…

export

C++’s export keyword was originally meant to permit the separation of the definition of a template from its usages. It proved to be incredibly difficult to implement, and the “export templates” C++ feature was dropped in C++11, having never seen widespread usage.

It remained a reserved identifier for several years, and now it returns with renewed purpose and meaning for C++ Modules.

The New and Improved (and Completely Repurposed) export

The export keyword may only be used in a module interface unit. The keyword is attached to a declaration of an entity, and causes that declaration (and sometimes the definition) to become visible to module importers.

(The only weird one is the export keyword in the module-declaration, which is just a re-use of the keyword and does not actually “export” any entities.)

What Can I export?

This isn’t the wild west, and export isn’t free to be plastered everywhere, namely:

  1. You cannot export entities with internal linkage (static variables and functions; and functions, variables, and classes defined within an anonymous namespace):

    namespace {
    
       // ILLEGAL! This is an anonymous namespace
       export void do_stuff() {
           // ...
       }
    
       // ILLEGAL! This is an anonymous namespace
       export int five = 5;
    
       // ILLEGAL! This is an anonymous namespace
       export class stuff {
           // ...
       };
    
    }
    
    // ILLEGAL! This is declared `static`
    export static void do_more_stuff() {
        // ...
    }
    
    // ILLEGAL! This is declared `static`
    export static int twelve = 12;
    
  2. An exported declaration must actually declare something.
  3. The first declaration of an exported entity must be an exported declaration. Subsequent declarations and definitions need not have the export keyword:

    export class Thing;  // Good
    
    export class Thing;  // Okay, but redundant
    
    class Thing;  // Implicit `export` keyword
    
    class Thing { // Implicit `export` keyword
        int a;
        int b;
    };
    
    class SomethingElse; // Good. Not exported.
    
    export class SomethingElse; // Illegal! First declaration is not exported!
    
  4. Exported declarations may only appear at namespace-scope (this includes the global namespace).

    export class Stuff { // Okay
        int a;
        int b;
    };
    
    export class MoreStuff {
        int a;
        export int b; // lolwut? Illegal.
    };
    
    export void foo() {
        export int value = get_value(); // wat. No!
    }
    
    export void bar(export std::string name) { // Please reconsider.
        // ...
    }
    
    template <export typename T>  // plz stop
    export class my_container {};
    
  5. A using-declaration may be exported unless the referred entity has internal or module linkage. (“module linkage” will be covered in a future post). A using namespace declaration cannot be exported.

    namespace Stuff {
       export class Widget {};
    
       class Gadget {};
    
       namespace {
    
          class Gizmo {};
    
       } // namespace
    
    } // namespace Stuff
    
    export using Stuff::Widget; // Okay
    
    export using Stuff::Gadget; // Not okay
    export using Stuff::Gizmo; // Bad
    export using namespace Stuff; // Nope
    
  6. A namespace definition or linkage-specification block may be exported, but all entities declared within the corresponding declaration block must adhere to the above rules.

    export namespace foo {
    
       int eight = 8; // Okay. `eight` is exported as `foo::eight`.
    
       static int nine = 0; // Illegal!
    
       namespace {
    
          void do_stuff() { // Stop! You have violated the law. Pay the court a fine or serve your sentence! Your stolen goods are now forfeit.
    
          }
    
       }
    
    }
    

    Note that this rule only applies to the declaration block immediately following the export declaration. This is permissible:

    
    export namespace foo {
    
       int two = 2; // Okay
    
    }
    
    namespace foo {
    
       static int six = 6; // Also okay!
       // ^ This is not within an exported namespace definition, even though the
       // containing namespace `foo` itself is exported by another namespace
       // definition
    
    }
    
    
  7. In addition to exporting namespaces definitions and linkage specification blocks, one can also use a bare export block:

    export {
    
       class Aardvark;
    
       void eat_ants(Aardvark&);
    
    }
    

    Everything within an export block is exported. An export block does not introduce a new scope or change the linkage of its contents. The contents of an export block must follow the same rules as an exported namespace definition (all contained declarations must be exportable).

Aside: A strange (and unfortunate) side-effect of combining rule #6 with rule #2: static_assert may not at the top level of an exported namespace definition. Despite looking like a statement, static_assert is defined as a declaration by the C++ grammar, which permits it to appear in any place that a declaration is valid (otherwise you could not static_assert at namespace scope or within the body of a class definition). Because static_assert does not declare a named entity, exporting a static_assert violates rule #2. Because rule #6 requires all declarations within the namespace definition to be exportable, and static_assert is not exportable, we find that static_assert may not appear within an exported namespace definition. This is a very strange quirk, and to the best of my knowledge there is some effort to add an exception forstatic_assert.

There are a few more important restrictions on export, but they will be covered in a later post. This current understanding will work for now.

Implicit export-ing

There are two important cases where an entity is implicitly exported because of the exported-ness of a separate entity:

  1. An exported namespace definition or linkage-specification-block causes every declaration within the immediately following declaration block to be exported.

    export namespace Things {
    
       class Widget { // Implicitly exported as `Things::Widget`
    
       };
    
       void foo() { // Implicitly exported as `Things::foo`
    
       }
    
    }
    
    namespace Items {
       export extern "C" {
          void do_stuff(); // Implicitly exported as Items::do_stuff
       }
       export extern "C++" {
          void do_other_things(); // Implicitly exported as Items::do_other_things
       }
    }
    
  2. An exported entity implicitly exports the containing namespace:

    namespace baz {
    
       export void quux(); // Exported as baz::quux, and namespace `baz` is now exported
    
    }
    

Note that having an exported declaration within a namespace definition is not the same as having export on the namespace definition:

namespace europe {

   export class france; // `europe::france` is exported,
                        // and namespace `europe` is exported.

   class italy; // Not exported, even though the namespace is exported.

   namespace {

      class germany; // Perfectly legal. This is not an
                     // attempt to export an entity with
                     // internal linkage.

      static_assert(true, "Sane"); // Okay.

   }

}

Read this as: europe is an exported namespace, and its namespace definition contains exported entities, but the above is not an exported namespace definition.

export import?

In the last post, we saw export import used with module partitions:

export module my_module;
// Add `some_partition` to our module interface:
export import :some_partition;

But export import may also appear on regular imports:

export module my_module;

export import widgets_inc;

The result is that users who import my_module will “implicitly” import widgets_inc. This relationship is fully transitive.

Rules Regarding import

export has been a reserved word for many years, but now we have a new contextual keyword: import. We’ve seen it quite a bit, and it is mostly self-explanatory.

However, just like with export, there are some rules about using import:

  1. In a module unit, all imports must appear before any declarations in that module unit. You cannot import at arbitrary points in a module unit.

    export module yo;
    
    import dogs;
    
    void pet(dog& d);
    
    import cats; // Not allowed! Move this import above `pet`
    

    import is a special identifier, and it is still possible to use import as a name for other entities:

    export module yo;
    
    import dogs;
    
    class import {};
    
    import i1; // Illegal! `import` declarations must appear in the preamble
    ::import i2; // Okay. Declares a variable `i2` of type `import`.
    
    class Widget {
        import member; // Okay. No scope-resolution needed.
    };
    

    In a non-module translation unit, import may appear after declarations in the translation unit.

  2. import declarations may only appear at global scope.
  3. An import that names a module partition may only name partitions that belong to the same module which contains the import.
  4. A module may not import itself.
  5. A module unit A may not have an interface dependency on itself (a cyclic import) (see below regarding “interface dependencies).

Even though you are still able to use import as an identifier, please don’t, for the sake of our (and your own) sanity.

(As far as this author can determine, a namespace-scope declaration of unqualified type import or function returning unqualified type import is the only instance of C++ Modules breaking existing code.)

The Implicit import

There is one scenario in which an import implicitly occurs:

// europe.cpp
export module europe;

struct Country {};
struct France;
struct Germany;
// europe_impl.cpp
module europe;

struct France : Country {};
struct Germany : Country {};

If you recognize this from the prior post, I referred to such module units as europe_impl.cpp as anonymous implementation units. This name isn’t a name provided by the standard document, but I find it a useful shorthand when talking about “module implementation units that are not partitions.”

You’ll notice above that europe_impl.cpp refers to struct Country in the definitions of France and Germany, but it never imports anything!

“Anonymous” module implementation units are defined to have an “implicit” import of the module in which they belong. In fact: Adding an import europe is not allowed (and, because of the implicit import, not necessary).

You might think this could create a cyclic import, but it cannot: There is no way for any other module unit to import an implementation unit with no partition name. Its contents are unreachable from any other translation unit. Even the rest of europe cannot get at the contents of this file. Only within europe_impl.cpp will France and Germany be complete types (they will be seen as incomplete types everywhere else).

Interface Dependencies

This aspect is best explained by simply pasting the relevant text from the specification:

A translation unit has an interface dependency on a module unit U if it contains a module-declaration or module-import-declaration that imports U or if it has an interface dependency on a module unit that has an interface dependency on U.

Suppose the following program:

// a.cpp
export module Foo;
// b.cpp
export module Bar;

import Foo;
// c.cpp
import Bar;

c.cpp has an interface dependency on b.cpp by virtue of importing the module of which that module unit is a member. Even though it does not name a.cpp or the module thereof, it does have an interface dependency on a.cpp because a.cpp is an interface dependency of b.cpp. Interface dependencies are transitive.

The Dungeon Boss: visible versus reachable

C++ Modules introduces two new concepts: visibility and reachability, which help distinguish between what code is semantically valid in the face of (possibly implicitly) imported constructs.

What is Visibility?

Let’s look at the simplest visibility rule of all:

// main.cpp

#include <iostream>

const char* get_phrase() {
    return "Hello, world!";
}

int main() {
    std::cout << get_phrase() << '\n';
}

We’re not even using any module features here, but the rules of visibility and reachability have an effect on all code. In this sample, get_phrase is (obviously) “visible” from main.

A name is visible at a point within a translation unit if that name has been declared at an earlier point within the same translation unit. This is unchanged from C++17: Namespace-scope declarations are still not “hoisted”!

There are other situations in which visibility comes into play:

export module speech;

export const char* get_phrase() {
    return "Hello, world!";
}
// main.cpp
import speech;

import <iostream>;

int main() {
    std::cout << get_phrase() << '\n';
}

In this sample, get_phrase is visible within main.cpp because of the import speech; declaration.

“Visible” means “a candidate for name lookup.” Visibility applies to more than just namespace-scope items:

export module speech;

export struct Phrase {
    const char* spelling = nullptr;
};

export Phrase get_phrase() {
    return Phrase{"Hello, world!"};
}
// main.cpp
import speech;

import <iostream>;

int main() {
    Phrase phr = get_phrase();
    std::cout << phr.spelling << '\n';
}

In this sample, Phrase::spelling is also visible, which allows us to ask for the member when we have an instance of that object.

A Trickier Beast: Reachability

C++ Modules introduce a new concept of reachability. When an entity is reachable, the semantic properties of that entity are available, but not necessarily visible to name lookup. It is essential to understand the following:

  1. Every visible entity is also reachable.
  2. Being reachable does not imply being visible.
  3. Whether an entity is declared with export has no effect on if it is reachable: It only effects whether it is visible.
  4. If a class or enumeration type is reachable, then its members become visible, even if the containing name is not visible.

Point 4 has some interesting implications: We can have access to the semantic properties and member names of a class even if we cannot name that class.

This allows for some crazy 1337 h4cks:

export module Secrets;

// NOT EXPORTED!
class SecretClass {
public:
    explicit SecretClass(int i) : value(i) {}
    SecretClass(const SecretClass&) = delete;
    SecretClass(SecretClass&&) = default;

    int value = 0;
};

// Export a function that returns our non-exported class type
export SecretClass get_secret() {
    return SecretClass{42};
}
import Secrets;

void foo() {
    // ILLEGAL: `SecretClass` is not visible:
    SecretClass s1 = get_secret();

    // Okay: `SecretClass`'s move-constructor is *reachable*:
    auto s2 = get_secret();

    // Okay: The members of the class are *visible*
    int secret_value = s2.value;

    // ILLEGAL: `SecretClass` is not copyable:
    auto s3 = s2;

    // Okay: A move-construction of `SecretClass`:
    auto s4 = std::move(s2);

    // WHOA: Okay: Grab the class and give it a name.
    using NamedClass = decltype(s2);

    // Okay: The constructor of `SecretClass` is reachable, and we've now got
    // a name on the class.
    NamedClass s5{53};
}

You might believe that this is wildly unprecedented, but we’ve had a similar concept of “usable but not visible” since C++14, no modules required:

auto foo() {
    struct Money {
        int amount;
    };
    return Money{ 12 };
}

int bar() {
    auto f = foo();
    using SecretType = decltype(f);
    SecretType my_money{34};
    return my_money.amount; // Returns 34!
}

The Easy Case: Necessary Reachability

The standard specifies a sub-category of reachability called necessary reachability. Being necessarily reachable is an attribute of a translation unit and propagates to the entities declared within it (with some limitations, explored in a later post).

The rules of necessary reachability are simple: A translation unit is necessarily reachable if and only if it is a module interface unit on which the requesting translation unit has an interface dependency.

// eurasia_base.cpp
export module eurasia:base;

import <string>;

struct Country {
    std::string common_lang;
};
// eurasia_west.cpp
export module eurasia:west;

import :base;

struct Spain : Country {
    Spain() : Country{"es"} {}
};
struct France : Country {
    France() : Country{"fr"} {}
};
// eurasia_east.cpp
export module eurasia:east;

import :base;

struct Japan : Country {
    Japan() : Country{"jp"} {}
};
struct Russia : Country {
    Russia() : Country{"ru"} {}
};
// eurasia.cpp
export module eurasia;

import <memory>;

import export :base;
import export :east;
import export :west;

export const Country& get_country() {
    // ...
}
// main.cpp
import eurasia;

import <iostream>;

int main() {
    auto& c = get_country();
    std::cout << "Country language is " << c.common_lang << '\n';
}

Even though Country is not exported (nor visible), c.common_lang is valid for the following reasons:

  1. Every module unit in eurasia is an interface dependency of main.cpp.
  2. All partitions in eurasia are interface partitions
  3. Therefore: every module unit (and the things declared within) are necessarily reachable from main.cpp.
  4. By being reachable, the members of Country become visible to main.cpp.

The name of “necessary” reachability is to convey the idea that these rules enforce a baseline behavior for the propagation of semantic properties between translation units in a modularized program.

So… What would be “unecessarily” reachable?

That’s implementation defined. Brace yourself.

GOTCHA!

An astute observer will notice that the partitions :base, :east, and :west do not actually export anything, despite themselves being interface partitions (with the export module declaration).

You might therefore assume it safe to simply remove the export keyword from the module declaration, right?

Not so fast!

Remember the rule of necessary reachability:

A translation unit is necessarily reachable if and only if it is a module interface unit on which the requesting translation unit has an interface dependency.

Note that this only applies to interface module units. If we remove the export keyword from the module declaration, the module partition becomes an implementation partition.

The standard goes on to say this:

It is unspecified whether additional translation units on which the […] [translation unit] has an interface dependency are considered reachable, and under what circumstances.

[The edits and elisions will be covered in a later post.]

It also features two non-normative notes:

Implementations are therefore not required to prevent the semantic effects of additional translation units involved in the compilation from being observed.

It is advisable to avoid depending on the reachability of any additional translation units in programs intending to be portable.

This means that removing the export keyword in the declaration export module eurasia:base; might or might not break main.cpp!

We haven’t much deployment experience to say whether this caveat will be extremely relevant, and it only manifests in an already-fairly-contorted program.

Nevertheless: Heed this warning: Do not depend on arbitrary things being reachable beyond what is necessarily reachable.

A Short Reprieve

Would you believe there is still more to cover? Well, there is! It’ll have to wait for another day, though. In part three we’ll cover some still less-well-known features and facts about C++ modules. Hopefully you won’t have to wait an entire month to see it.

Secret Boss Battle

The Secret Bonus Boss Battle

CAUTION:

This section discusses an aspect of C++ Modules that is still under construction, but it can be used to give a good glance into the nitty-gritty details of the kind of problems that C++ Modules has to solve.

The particular aspect discussed here will almost certainly be changed before the final Standard ships.

If you don’t care to see what may never be (or if you want to sleep peacefully tonight) then skipping this section is recommended. Consider this your warning.

We know that we cannot export entities with internal linkage. It is explicitly banned:

export module Foo;

namespace {
   export class LolWatchThis {  // NOPE!
       static void say_hello() {
           std::cout << "Hello, everyone!\n";
       }
   };
}

LolWatchThis has “internal” linkage, and it is not allowed to be exported. In addition, its definitions are not visible to external translation units, unless…

export module Foo;

import <iostream>;

namespace {
   class LolWatchThis {
       static void say_hello() {
           std::cout << "Hello, everyone!\n";
       }
   };
}

export LolWatchThis lolwut() {
    return LolWatchThis();
}
// main.cpp
import Foo;

int main() {
    auto evil = lolwut();
    decltype(evil)::say_hello(); // WORKS ??
}

This is a phenomenon known as linkage promotion, discussed in p1395.

I Don't Get It

If you find this weird and frightening: You’re in good company. This behavior will almost certainly be changed with the adoption of p1498, which proposes to restrict the usage of TU-local (internal linkage) entities within contexts in which the definition may need to be exposed. Directly exposing internal entities to the outside world would be restricted (returning a TU-local type), but so would usage of those names in which the definition might leak silently (i.e. inline functions, even if the inline is implicit (e.g. function templates, in-class definitions of member functions, constexpr and consteval functions)).

A related paper, p1604, could tweak the meaning and/or occurrence of implicit inline, which could have a huge effect on the restrictions of p1498. I won’t write more here, as these changes are still in flight. A much-much later post may follow up on the resolutions to these issues.