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.
Understanding C++ Modules In C++20 (2)的更多相关文章
- Understanding C++ Modules In C++20 (1)
Compiling evironment: linux (ubuntu 16.04)+ gcc-10.2. The Post will clarify and discuss what modules ...
- C++20 的 Modules
最近看了两篇关于 C++ 20 Modules 很有意思的文章,戳: <Understanding C++ Modules: Part 1: Hello Modules, and Module ...
- ODOO-10.0 错误 Could not execute command 'lessc'
2017-01-05 20:24:12,473 4652 INFO None odoo.service.db: Create database `hello`. 2017-01-05 20:24:16 ...
- Cheatsheet: 2015 12.01 ~ 12.31
Mobile Setting Up the Development Environment iOS From Scratch With Swift: How to Test an iOS Applic ...
- ubuntu下简单的驱动编译
转自:http://www.eefocus.com/jefby1990/blog/13-02/291628_c39b8.html 本文是参考了网上多篇帖子而写的算不上什么原创.唯一值得欣慰的只不过在本 ...
- Linux内核树的建立-基于ubuntu系统
刚看 O'REILLY 写的<LINUX 设备驱动程序>时.作者一再强调在编写驱动程序时必须 建立内核树.先前的内核只需要有一套内核头文件就够了,但因为2.6的内核模块吆喝内核源码树中的目 ...
- vTPM环境部署(ubuntu)
注:1.系统:ubuntu16.04LTS2.ISO镜像:/home/huanghaoxiang/ubuntu-server.iso3.IMG路径:/home/TPM-Machine4.Login: ...
- 如何开发由Create-React-App 引导的应用(二)
此文章是翻译How to develop apps bootstrapped with Create React App 官方文档 系列文章 如何开发由Create-React-App 引导的应用 如 ...
- dfsdf
This project was bootstrapped with Create React App. Below you will find some information on how to ...
随机推荐
- Estimation of Non-Normalized Statistical Models by Score Matching
目录 概 主要内容 方法 损失函数的转换 一个例子 Hyv"{a}rinen A. Estimation of Non-Normalized Statistical Models by Sc ...
- <学习opencv>图像和大型阵列类型
OPenCV /*=========================================================================*/ // 图像和大型阵列类型 /* ...
- Nginx部署及Web基础
目录 Nginx部署及Web基础 Nginx简介 Nginx特点 Web服务 Web服务器软件 Nginx和Apache对比图 部署Nginx yum安装 编译安装 平滑增加Nginx模块 Nginx ...
- 如何使用NiFi等构建IIoT系统
您认为构建一个先进的工业物联网原型需要多长时间: 从传感器收集数据到每个工厂的网关 将传感器数据从一个或多个工厂移至云或数据中心 自动热部署新配置到所有边缘设备 支持大规模数据量和端到端安全性 使用正 ...
- 很漂亮的一个背景控件——ribbons.js
写博客的人都喜欢优化自己的博客主页,博主也一样,找了一些背景控件,像canvas-nest.js等等,最终选择了ribbons.js,并基于源码,稍作了一丁点的修改,这里分享出来 (function ...
- STL(1)vector
STL(1) 1.vector vector是vector直译为"向量",一般说成"变长数组",也就是长度根据需要而自动改变的数组,有些题目需要开很多数组,往往 ...
- 初识python:多线程
多线程:在一个程序中,独立运行的程序片断叫作"线程"(Thread),利用它编程的概念就叫作"多线程处理".即:一个进程中,多个线程. 举个例说明:就像是一列火 ...
- 远程连接PostgreSQL
在华为云上安装了PostgreSQL,本地使用pgAdmin客户端来访问PostgreSQL 首先,需要在华为云服务器上,放开访问PostgreSQL的5432端口,否则会报请求超时 通过创建安全组来 ...
- 解析HetuEngine实现On Yarn原理
摘要:本文介绍HetuEngine实现On Yarn的原理,通过阅读本文,读者可以了解HetuEngine如何在资源使用方面融入Hadoop生态体系. 本文分享自华为云社区<MRS HetuEn ...
- [源码分析] Facebook如何训练超大模型---(1)
[源码分析] Facebook如何训练超大模型---(1) 目录 [源码分析] Facebook如何训练超大模型---(1) 0x00 摘要 0x01 简介 1.1 FAIR & FSDP 1 ...