Understanding C++ Modules In C++20 (2)

Compiling evironment: linux (ubuntu 16.04)+ gcc-10.2.

The post will focus on using export,import,visible and reachable.

1 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.

2 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.

3 What Can I export?

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.  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.

4 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.

5 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.

6 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.)

7 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).

8 Interface Dependencies

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

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.

9 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.

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.

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.

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.

posted @ 2021-03-29 15:22  钟齐峰  阅读(112)  评论(0编辑  收藏  举报