Understanding C++ Modules: Part 2: export, import, visible, and reachable
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.)
Posts in this series:
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:
-
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;
- An exported declaration must actually declare something.
-
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!
-
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 {};
-
A
using
-declaration may be exported unless the referred entity has internal or module linkage. (“module linkage” will be covered in a future post). Ausing 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
-
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 }
-
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 anexport
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:
-
An
export
ed 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 } }
-
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
:
-
In a module unit, all
import
s must appear before any declarations in that module unit. You cannotimport
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 useimport
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. import
declarations may only appear at global scope.- An
import
that names a module partition may only name partitions that belong to the same module which contains theimport
. - A module may not
import
itself. - 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 importsU
or if it has an interface dependency on a module unit that has an interface dependency onU
.
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:
- Every visible entity is also reachable.
- Being reachable does not imply being visible.
- Whether an entity is declared with
export
has no effect on if it is reachable: It only effects whether it is visible. - 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:
- Every module unit in
eurasia
is an interface dependency ofmain.cpp
. - All partitions in
eurasia
are interface partitions - Therefore: every module unit (and the things declared within) are
necessarily reachable from
main.cpp
. - By being reachable, the members of
Country
become visible tomain.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.
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 export
ed.
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.
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.