Daniela Engert - Meeting C++ 2021
Modules demystified and applied
a short recap
source.cpp
some header.h
library.h
translation unit
object file
library.h
library interface
library.cpp
library.h
library implementation
library object file
other header.h
program
other header.h
other header.h
Barrier→║
traditional library
library.cpp
source.cpp
library.h
other header.h
declarations
macros
compiler options
predefined,
commandline
defaults,
commandline
none
translation unit
files
object file
discarded
discarded
some header.h
source.cpp
import library;
translation unit
object file
export module library;
library interface unit
module library;
library implementation
library implementation object file
program
library interface object file
export ...;
export ...;
BMI
BMI
Barrier→║
Architected→→
modularized library
some
all
interface.cpp
declarations
macros
compiler options
predefined,
commandline
defaults,
commandline
none
module interface unit
files
object file
BMI file
discarded
module;
#include <vector>
export module my.first_module;
export import other.module;
#include "mod.h"
constexpr int beast = 666;
export std::vector<int> frob(S);
module;
#include <vector>
module my.first_module;
std::vector<int> frob(S s) {
return {s.value + beast};
}
// mod.h
#pragma once;
struct S {
int value = 1;
}
module purview
global module fragment
module declaration without a name
global module
default name 'domain'
(primary) module interface unit
The name of this module can be referred to only in
module implementation unit
exported
"exportedness" applies to names
Interface unit | ✅ | ✅ | ✅ | ❎ | ✅ | ✅ | ✅ | ||
Implementation unit | ✅ | ✅ | ❎ | ✅ | |||||
Interface partition | ✅ | ✅ | ❎ | ✅ | ✅ | ✅ | |||
Implementation partition | ✅ | ❎ | ✅ | ❎ | ✅ | ||||
Private module fragment | ✅ | ✅ | |||||||
Header unit | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Defines interface
contributes to interface
implicitly imports interface
part of module purview
part of global module
exports MACROs
creates BMI
contributes to BMI
fully isolated
✅ unconditionally ❎ if a GMF exists in the TU / if TU's BMI is imported into the primary module interface
This zoo of module TU types allow for various module structures:
module; // GMF
#include <vector>
export module SimpleModule;
// non-exported declarations
struct detail {
int answer = 42;
};
export
namespace SimpleModule {
void f(const detail &);
std::vector<detail> g();
}
module SimpleModule;
namespace SimpleModule {
void f(const detail & D) {
// do something with D
}
}
module;
#include <vector>
module SimpleModule;
namespace SimpleModule {
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
formerly the interface header formerly implementation sources for f() and g()
export module LargeModule;
export import : iface.f;
export import : iface.g;
module LargeModule : impl.f;
import : iface.f;
namespace LargeModule {
void f(const detail & D) {
// do something with D
}
}
module;
#include <vector>
module LargeModule : impl.g;
import : iface.g;
namespace LargeModule {
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
export
module LargeModule : iface.f;
import : detail;
namespace LargeModule {
export
void f(const detail & D);
}
module;
#include <vector>
export
module LargeModule : iface.g;
import : detail;
namespace LargeModule {
export
std::vector<detail> g();
}
module LargeModule : detail;
// non-exported declarations
struct detail {
int answer = 42;
};
export module HierModule;
export import HierModule.f;
export import HierModule.g;
module HierModule.f;
import HierModule.detail;
namespace HierModule {
void f(const detail & D) {
// do something with D
}
}
module;
#include <vector>
module HierModule.g;
import HierModule.detail;
namespace HierModule {
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
export
module HierModule.f;
import HierModule.detail;
namespace HierModule {
export
void f(const detail & D);
}
module;
#include <vector>
export
module HierModule.g;
import HierModule.detail;
namespace HierModule {
export
std::vector<detail> g();
}
export module HierModule.detail;
struct detail {
int answer = 42;
};
module;
#include <vector>
export module SmallModule;
struct detail {
int answer = 42;
};
export
namespace SmallModule {
void f(const detail & D) {
// do something with D;
}
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
module;
#include <vector>
export module SingleFileModule;
struct detail { // not exported but reachable
int answer = 42;
};
export namespace SingleFileModule {
void f(const detail & D);
std::vector<detail> g();
}
module : private; // neither exported nor reachable!
namespace SingleFileModule {
void f(const detail & D) {
// do something with D;
}
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
// header 'detail.h'
#pragma once;
struct detail {
int answer = 42;
};
// header 'header_f.h'
#pragma once;
#import "detail.h"
namespace IndependentHeader {
void f(const detail & D) {
// do something with D
}
}
// header 'header_g.h'
#pragma once;
#include <vector>
#import "detail.h"
namespace IndependentHeader {
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
// header 'detail.h'
#pragma once;
struct detail {
int answer = 42;
};
// header 'header_f.h'
#pragma once;
#include "detail.h"
namespace COmposedHeader {
void f(const detail & D) {
// do something with D
}
}
// header 'header_g.h'
#pragma once;
#include <vector>
#include "detail.h"
namespace ComposedHeader {
std::vector<detail> g() {
return {{ 42 }, { 43 }};
}
}
// header 'composed.h'
#pragma once;
#include "header_f.h"
#include "header_g.h"
hide and seek
// translation unit 1
int i; // point of declaration (POD), introduces entity 'i'
int j = i; // POD, introduces entity 'j'
// point of look-up (POL), names visible entity 'i'
int k = l; // POD, introduces entity 'k'
// POL, names invisible entity 'l'
// entity 'l' is not yet declared
// (relative invisibility)
int l; // point of declaration (POD), introduces entity 'l'
int m = n; // POD, introduces entity 'm'
// POL, names invisible entity 'n'
// entity 'n' is declared in a different TU
// (total invisibility)
// translation unit 2
int n; // POD, introduces entity 'n'
global scope
Lookup of entities at global scope
relative to their point of declaration
template <typename T>
int foo(T t) {
return t.value_; // POL ?, names not yet visible entity 'value'
} // at class scope of dependent, visible entity 't'
struct S {
int value_ = 1; // POD, introduces entity 'value'
}; // at class scope of 'S'
int x = S{}.value_; // POL, names visible entity 'value' at
// class scope of visible entity 'S
x = foo(S{}); // POL !, names visible entity 'value' at
// class scope of visible entity 'S'
struct S {
int foo() {
return value; // POL ?, names not yet declared entity 'value'
} // at class scope of visible entity 'S'
int value = 1; // POD, introduces entity 'value'
// at class scope of 'S'
}; // POL !, names visible entity 'value'
// at class scope of visible entity 'S'
class scope
Lookup of entities at class scope
from outside the class
Lookup of entities at class scope
from within the class
namespace N {
int n = 1; // POD, introduces entity 'N::n'
class S {
int j_ = 0;
friend int f(S s) { // POD, introduces entity 'N::f', declared as friend from within
return s.j_; // class scope 'N::S', → so-called "hidden friend"
}
};
namespace M {
void n(); // POD, introduces entity 'N::M::n'
int x = n; // POL, names entity 'n', unqualified lookup (UL)
// FAIL: entity 'N::M::n' hides entity 'N::n' from UL
auto y = &f; // POL, names entity 'f', UL
// FAIL: entity 'N::f' is invisible to UL
auto z = &S::f; // POL, names entity 'f' in scope 'S', class member lookup (CML)
// FAIL: entity 'N::f' is not member of class S
auto z = &N::f; // POL, names entity 'f' in scope 'N', qualified lookup (QL)
// FAIL: entity 'N::f' was not declared in scope 'N' but scope 'S'
int a = N::n; // POL, names entity 'n' in scope 'N', QL
int b = f(S{}); // POL, names entity 'n' using argument of type S, ADL
} // namespace M
} // namespace N
visibility, it depends on how you look
Entities may become hidden (i.e. invisible to lookup) by
but become visible again by selecting the appropriate lookup algorithm
auto foo(int x) {
struct S { // POD, introduces entity 'S' in function block scope
int s_;
};
return S{x}; // POL, names entity 'S', part of the function's signature
}
auto what = foo(1);
assert(what.s_ == 1);
static_assert(std::is_same_v<decltype(what), ❓::S>); // POL, names invisible entity 'S'
near total invisibility
Even though name 'S' is visible at function block scope and it is the function's return type, it is totally invisible from outside the function.
This is the best you can achive in terms of name hiding in a TU. Alas, it denies foo forward-declarability from another TU despite having external linkage.
// client TU
import M; // introduces entity 'foo' by BMI, exported from module M
auto y = foo(1); // POL, names entity 'foo'
// the name of result type of 'foo' is totally invisible!
// module interface TU
export module M;
struct S { // POD, introduces entity 'S', not exported
int s_ = 1;
};
export S foo(int); // POD, introduces entity 'foo' and exports name 'foo'
// POL, names visible entity 'S'
// module implementation TU
module M;
S foo(int x) { // POL, names visible entities 'S' and 'foo'
return S{x};
}
of declarations
export module mod; // may become "necessarily reachable" if the interface of 'mod' is imported
import stuff; // not exported, implementation detail, not part of module interface
// creates "interface dependency" to 'stuff' which is "necessarily reachable"
struct S { // not exported, not meant to be used elsewhere outside
S(const char * msg) : sth_{ msg } {}
auto what() const { return sth_.message(); }
something sth_; // there must be 'something' exported from module 'stuff'
};
export // exports name 'foo'
S foo(const char * msg) {
return { msg };
}
#include <type_traits>
import mod; // creates "interface dependency" to 'mod' and 'stuff'
// makes 'mod' "necessarily reachable"
int main(int, char * argv[]) {
const auto result = foo(argv[0]); // so far, so good
const auto huh = result.what(); // why is entity 'what' nameable? 🤔
using mystery = decltype(huh); // it was never exported from 'mod'
// -> it's a reachable semantic property of 'S'!
static_assert(std::is_class_v<mystery>); // compiles
static_assert(sizeof(mystery::value_type) == sizeof(char)); // compiles
return huh.empty(); // compiles
}
module;
#include <string>
export module stuff;
namespace detail { // not exported
struct base { // not exported
base(const char * msg) : msg_{ msg } {}
std::string message() const { return msg_; }
const char * msg_;
};
} // namespace detail
export
struct something : detail::base {
something(const char * msg) : detail::base{ msg } {}
};
// stuff that's not used in this particular example
namespace detail { // not exported
struct expendable { // totally unused and never was
void get_rid_of_me() { /* TBD */ }
};
} // namespace detail
export
struct other_stuff : detail::base, detail::expendable {
bool doit() const noexcept; // implemented elsewhere
};
There is a dependency chain between TUs:
And there's a dependency chain between multiple declarations / definitions:
All declarations along such dependency chains and their semantic properties are required to give meaning to the source code.
Therefore it is necessary that the compiler can reach all these declarations (and possibly their definitions) regardless of the source / header file where they textually exist.
These declarations are said to be "necessarily reachable".
This also implies that the BMIs of imported module interfaces, imported module partitions, and imported header modules containing these declarations most be (recursively) available.
Other declarations and semantic properties contained in these BMIs that are not part of dependency chains established during the compilation of the current TU might be reachable, too.
The reachability of non-necessary declarations is implementation-defined!
Depending on the entity that is declared, a declaration brings a set of semantic properties with it:
If a declaration is also a definition, the set of semantic properties is augmented by e.g. a function body or a class body that may contain declarations themselves.
// header file "some.h"
extern "C++" { // default
using func = int(int) // mandatory
noexcept(false); // default
struct D // mandatory
{ // optional or mandatory
operator int() const { return v_; }
int v_ = 1;
};
extern "C" { // optional
namespace N { // mandatory
func bar; // mandatory
}
[[nodiscard]] // optional
extern // default
inline // optional or mandatory
int N::bar(int // mandatory
x = D{}) // optional or mandatory
{ // optional or mandatory
return x + D{2};
}
}
}
extern "C++" {
}
}
}
using func =
extern "C" {
[[nodiscard]]
int(int)
noexcept(false)
struct D
}
;
{
operator int() { return v_; }
int v_ = 1;
namespace N {
func bar;
}
extern
int N::bar(int x
inline
= D()
return x + D{2};
)
{
// the function's mangled name is 'bar', not 'N::bar(int) !
about relationships between TUs
linkage determines the relationship between named entities within scopes of
non-modular C++ knows three kinds of linkage
Modules add a fourth kind of linkage
export module mod1;
int foo(); // module linkage
export namespace A { // external
int bar() { // external linkage
return foo();
}
} // namespace A
import mod1;
import mod2;
using namespace A;
int main(){
return bar() + baz();
}
no clash
export module mod2;
int foo(); // module linkage
namespace A { // external linkage
export int baz() { // external link.
return foo();
}
} // namespace A
same namespace ::A
name '::foo' is attached to module 'mod1', i.e. '::foo@mod1',
exported name '::A::bar' is also attached to the module
name '::foo' is attached to module 'mod2', i.e. '::foo@mod2'',
exported name '::A::baz' is also attached to the module
namespace name '::A' is attached to the global module, as it is oblivious of module boundaries
export module mod3;
int foo(); // module linkage, attached to module 'mod3'
namespace A { // external linkage, attached to global module
export int bar() { // external linkage, attached to module 'mod3'
return foo();
}
} // namespace A
msvc 17.2 name mangling:
?foo@@YAHXZ::<!mod3>
?bar@A@@YAHXZ::<!mod3>
clang13 & gcc(modules) name mangling:
_ZW4mod3E3foov _ZN1A3barEv
The module name will be encoded into the linker symbol if an entity has module linkage, and may be encoded if it is exported and therefore has external linkage
export module mod3;
namespace A {
export int bar() { // external linkage, attached to module 'mod3'
return foo();
}
} // namespace A
Strong ownership model, the linker symbols of exported names contain the module name they are attached to
e.g. msvc
?bar@A@@YAHXZ::<!mod3>
Benefit:
Identical names can be exported from multiple modules and used in separate TUs without causing linker errors.
Weak ownership model, the linker symbols of exported names are oblivious of module attachment
e.g. clang & gcc
_ZN1A3barEv
Benefit:
Exported names can be moved freely between modules or from headers into modules.
export module awesome.v1;
// other stuff, not exported,
// must go here because reasons
export namespace libawesome {
// implemented elsewhere
int deep_thought(...);
} // namespace libawesome
// Poor customer's application
#include "oem1/interface.h"
#include "oem2/interface.h"
int main(){
return OEM1::doit() +
OEM2::makeit();
}
compatible
export module awesome.v2;
// other stuff, not exported,
// must go here because reasons
export namespace libawesome {
// for compatibility
int deep_thought(...);
// implemented elsewhere
int much_deeper_thought(...);
} // namespace libawesome
// OEM 1, traditional header
// implementation calls 'deep_thought'
namespace OEM1 {
int doit();
} // namespace OEM1
// OEM 2, traditional header
// impl. calls 'much_deeper_thought()'
namespace OEM2 {
int makeit();
} // namespace OEM2
used in implementations of OEM1 and OEM2, not exposed
perfectly tested™
perfectly tested™,
distributed as static libraries & header
Compiles on platform A 😁
Links on platform A 😊
Profit! 🤑
Compiles on platform B 😁
Linker error on platform B 😱
but why? 🤔(weak ownership)
export module mod4;
int foo(); // module linkage, attached to module 'mod4'
extern "C" int bar(); // external linkage, attached to global module
extern "C++" int baz() { // external linkage, attached to global module
return foo() + bar();
}
msvc 17.2 name mangling:
?foo@A@@YAHXZ::<!mod4>
_bar ?baz@@YAHXZ
gcc(modules) name mangling:
_ZW4mod4E3foov bar _ZW4mod4E3bazv (wat? 🤔)
Giving explicit language linkage specifications reattaches the names to the global module and mangles them accordingly into linker symbols
Using an interface in compiled form rather than by textual inclusion
Exporting names selectively makes visibility an architectural decision rather than being technically unavoidable
The representation of declarations in an BMI and making them reachable guarantees identical interface semantics everywhere.
Augmenting linker symbols with their provenance make TUs less promiscuous.
The road forward
Options available and advice on how to proceed into the modules world:
For the most part, the code is located in 12 headers defining the API
Plus 2 source files that can be precompiled into a static or shared library
Question: how can this traditional library become a full-fledged module of the same name, i.e. become a dual-mode library?
Requirements:
Answer: this set of requirements is unattainable!
Neither a header module nor a named module has all necessary properties:
C++20 to the rescue: screw macros and support only the modern alternatives (i.e. UDLs) provided by the latest version of {fmt} !
Question: which implementation strategy? (refer to slides 9.x)
There is a lot of coupling between most headers because of
And this applies to the compilable sources just as well.
This is not bad per se if the library is seen as a whole and therefore it is not untypical in traditional libraries. If a clean, layered module structure is the primary goal, untangling that 'mess' becomes necessary.
Answer: restructuring a dual-mode library is probably not worth the effort as long as the details are clearly separatable from the API!
Strategy:
In other words:
module; // start of the 'global module fragment' (GMF)
// put *all* non-library headers (STL, platform headers, etc.) here
// to prevent further inclusion into the module purview!
#include <algorithm>
#include <sys/stat.h>
...
// end of external code attached to the 'global module'
export module fmt; // start of the 'module purview'
#define macros to differentiate between interface and details
// put *all* library headers here to become
// * the exported interface (API)
// * the non-exported, but reachable implementation details
#include "format.h"
#include "chrono.h"
...
// end of declarations affecting other TUs
module : private; // start of the 'private module fragment' (PMF)
// put *all* library sources here to become part of the compiled object file
// all required macros are available, yay!
#include "format.cc"
#include "os.cc"
export module fmt; // part of the module purview that affects other TUs
// macro definitions to differentiate between exported
// and non-exported sections in the code
#define FMT_MODULE_EXPORT export // these expand to nothing
#define FMT_MODULE_EXPORT_BEGIN export { // if used outside of module interface
#define FMT_MODULE_EXPORT_END } // in traditional library
#define FMT_BEGIN_DETAIL_NAMESPACE \ // expands to 'namespace detail {'
} \ // outside of module interface
namespace detail {
#define FMT_END_DETAIL_NAMESPACE \ // expands to '}'
} \ // outside of module interface
export {
#include "fmt/args.h" // the *full* API
#include "fmt/chrono.h"
#include "fmt/color.h"
#include "fmt/compile.h"
#include "fmt/os.h"
#include "fmt/locale.h"
#include "fmt/printf.h"
#include "fmt/xchar.h"
#include "fmt/ostream.h"
#include "fmt/ranges.h"
This single module interface TU compiles into two artefacts:
This is basically a unity build of the whole library that provides the full API.
The object file may then optionally be wrapped into a static or shared library.
Benefits of a dual-mode library:
On the journey to making {fmt} a full-fledged named module, I've encountered a couple of stumbling blocks that needed to be adressed.
The properties of module interfaces require special care when implementing them. Stuff that never had to be taken into consideration with headers becomes really important now!
Most of them are due to the separation of visibility of names when
This applies to both named modules and header units!
Instantiations of templates in user code perform name lookup of dependent entities from outside of the module. Non-exported names are invisible now and may cause compile failures.
export module M;
namespace detail {
template <typename T>
void baz(T) {}
template <typename T>
void bar(T t) {
baz(t); // ok while compiling the module interface
} // fails to find 'baz' when 'bar' is implicitly instantiated
} // namespace detail
export template <typename T>
void foo(T t) {
detail::bar(t); // ok, fully qualified name lookup is done at module compilation time
}
Two potential solutions:
export module M;
namespace detail {
template <typename T> void baz(T) {}
template <typename T>
void bar(T t) {
detail::baz(t); // do fully-qualified name lookup if you *really* mean
} // to call 'detail::baz' only (i.e. disable ADL)
} // namespace detail
...
export module M;
namespace detail {
void baz(int) {}
template <typename T>
void bar(T t) {
using detail::baz; // "symbolic link" 'detail::baz' (looked up at module compilation time)
baz(t); // into the function body (thereby available at template instantiation time)
} // if you want to make the call to 'baz' a customization point (i.e. enable ADL)
} // namespace detail
...
beware of entities with internal linkage at namespace-scope when importing headers as header-units.
This is quite common when defining constants.
// header file 'some.h'
static const int the_answer = 42; // internal linkage -> not exported from header unit
namespace {
constexpr int the_beast = 666; // internal linkage -> not exported from header unit
}
import "some.h" // import rather than #include!
int main() {
return the_answer; // name 'the_answer' is unknown because
} // it wasn't eligible to be exported from 'some.h'
Solution:
// header file 'some.h'
inline const int the_answer = 42; // define variable with external linkage
enum int_constants : int { // enum definition has external linkage
the_beast = 666;
};
struct constants { // struct definition has external linkage
static constexpr int no_answer = 0;
static constexpr unsigned pi = 4;
};
import "some.h" // import rather than #include!
int main() {
return the_answer; // name 'the_answer' is known now
}
Within the purview of a module, the 'inline' specifier gets its original meaning back!
Member bodies with function definitions at class scope are no longer implicitly 'inline'. You may give 'inline' hints if you mean it.
export module M;
struct S {
int foo() { return bar(); } // no longer implicitly 'inline',
// the function call might be
// invalid in module context!
inline int bar() { return 42; } // safe to inline
};
Beware of entities that are local to the TU.
Do not expose them e.g. by naming them in non-TU-local inline functions!
Learn more at [basic.link]#14 in the standard
export module M;
static void foo(); // TU-local
static inline void bar() { foo(); } // ok, TU-local
inline void baz() { bar(); } // error, 'baz()' has module linkage
// must not "expose" TU-local 'bar()'!
Within the purview of a module, the 'inline' specifier gets its original meaning back!
Member bodies with function definitions at class scope are no longer implicitly 'inline'. You may give 'inline' hints if you mean it.
export module M;
struct S {
int foo() { return bar(); } // no longer implicitly 'inline',
// the function call might be
// invalid in module context!
inline int bar() { return 42; } // safe to inline
};
A reality check
Let's examine them in detail.
// #include <...>, import <...>, import fmt; go here
// The fictitious code requires at least the API from fmt/format.h and fmt/xchar.h
int main {
/* empty main to zoom in on making the API available to TU
fictitious call:
fmt::print(L"The answer is {}", 42);
*/
}
Baseline, no #include or import:
compile time 31 ms
all configurations taken on an AMD Ryzen 9 5900X, compiled with msvc 16.11.3, release mode
#include <fmt/format.h>
#include <fmt/xchar.h>
int main {
/* empty main to zoom in on making the API available to TU
fictitious call:
fmt::print(L"The answer is {}", 42);
*/
}
Two configurations
header-only: compile time 944 ms (baseline + 913 ms), 6896 non-blank {fmt} code lines, 59'430 lines after preprocessing
static library: compile time 562 ms (baseline + 531 ms), 4685 non-blank {fmt} code lines, 42'735 lines after preprocessing
#include <fmt/format.h>
#include <fmt/xchar.h>
int main {
/* empty main to zoom in on making the API available to TU
fictitious call:
fmt::print(L"The answer is {}", 42);
*/
}
Two configurations
header-only: compile time 511 ms (baseline + 480 ms)
static library: compile time 304 ms (baseline + 273 ms)
Total std lib BMI size 41 MB (461 MB if std lib user-compiled from original headers)
import <fmt/format.h>;
import <fmt/xchar.h>;
int main {
/* empty main to zoom in on making the API available to TU
fictitious call:
fmt::print(L"The answer is {}", 42);
*/
}
Two configurations
header-only: compile time 64 ms (baseline + 33 ms), BMI size 22 MB
static library: compile time 64 ms (baseline + 33 ms), BMI size 16 MB
#include <fmt/args.h> // provide the *full* API
#include 8 more {fmt} headers here just as the named module does!
#include <fmt/xchar.h>
int main {
}
#include (header-only): 1599 ms (baseline + 1568 ms), 90'431 lines preprocessed
#include (static library): 1422 ms (baseline + 1391 ms), 88'576 lines preprocessed
Mod. STL (header-only): 658 ms (baseline + 627 ms), 10'249 code lines
Mod. STL (static library): 430 ms (baseline + 399 ms), 8'038 code lines
import (header-only): 160 ms (baseline + 129 ms), BMI size 117 MB
import (static library): 155 ms (baseline + 124 ms), BMI size 91 MB
import fmt;
int main {
/* empty main to zoom in on making the API available to TU
fictitious call:
fmt::print(L"The answer is {}", 42);
*/
}
Sorry, only one configuration with everything that {fmt} can provide!
Module interface unit: 10'672 non-blank code lines from {fmt}, 128'431 lines after preprocessing
compile time about 31 ms
There is no measurable difference to baseline!
#include <fmt/*.h> // provide the *full* API
import <fmt/*.h> // provide the *full* API
import fmt; // provide the *full* API
int main {
}
#include (header-only): 1599 ms (baseline + 1568 ms), 90'431 lines preprocessed
#include (static library): 1422 ms (baseline + 1391 ms), 88'576 lines preprocessed
Mod. STL (header-only): 658 ms (baseline + 627 ms), 10'249 code lines
Mod. STL (static library): 430 ms (baseline + 399 ms), 8'038 code lines
import (header-only): 160 ms (baseline + 129 ms), BMI size 117 MB
import (static library): 155 ms (baseline + 124 ms), BMI size 91 MB
named module: 31 ms (baseline + <1 ms), BMI size 8 MB
This is the way! 128'431 lines preprocessed
#include
import
bumpy roads ahead ...
gcc (trunk) | clang | msvc | |
---|---|---|---|
Syntax specification | C++20 | <= 8.0: Modules TS >= 9.0: TS and C++20 |
<= 19.22: Modules TS >= 19.23: C++20 |
Named modules | ✅ | ✅ | ✅ |
Module partitions | ✅ | ⛔ | ✅ |
Header modules | 🟩 (undocumented) | 🟩 (undocumented) | ✅ |
Private mod. fragment | ⛔ | ✅ | ✅ |
Name attachment | ✅ weak model | ✅ weak model | ✅ strong model |
#include → import | ⛔ | ⛔ | ✅ |
__cpp_modules | ⚠ 201810L | ⛔ | ✅ 201907L |
Modularized std library | ⛔ | ⛔ | 🟩 (experimental) |
Build systems with support for C++ modules are rare
Contact
Images: Bayeux Tapestry, 11th century, world heritage
source: WikiMedia Commons, public domain
Ceterum censeo ABI esse frangendam