Updated in 2023-07.
This article describes some Clang header modules
features that apply to #include. These features enforce a
more explicit dependency graph, which provide documentation purposes and
makes refactoring convenient. The benefits of clean header inclusions
are well described in Include
What You Use as well, so I won't repeat them here.
When using C++20 modules, these features apply to
#include in a global module fragment (module;)
but have no effect for import declarations.
Layering check
-fmodules-decluse
For a #include directive, this option emits an error if
the following conditions are satisfied (see
clang/lib/Lex/ModuleMap.cpp
diagnoseHeaderInclusion):
- The main file is within a module (called "source module", say,
A). - The main file or an included file from the source module includes a
file from another module
B. Adoes not have a use-declaration ofB(nouse B).
For the first condition, -fmodule-map-file= is needed to
load the source module map and -fmodule-name=A is needed to
indicate that the source file is logically part of module
A.
For the second condition, the module map defining B must
be loaded by specifying -fimplicit-module-maps (implied by
-fmodules and -fcxx-modules) or a
-fmodule-map-file=.
Here is an example:
1 | cat > a.cc <<'eof' |
The following commands lead to an error about dir/c.h.
#include "dir/b.h" is allowed because module A
has a use-declaration on module B.
1 | % clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.modulemap -fmodule-name=A -fimplicit-module-maps a.cc |
textual header "c.h" triggers the error as well.
If we remove -fmodule-name=A, we won't see an error:
Clang does not know a.cc logically belongs to module
A.
-fmodules-strict-decluse
This is a strict variant of -fmodules-decluse. If an
included file is not within a module, -fmodules-decluse
allows the inclusion while -fmodules-strict-decluse reports
an error.
Use the previous example, but drop
-fimplicit-module-maps and
-fmodule-map-file=dir/module.modulemap so that Clang thinks
dir/c.h is not within a module. Let's see the distinction
between -fmodules-decluse and
-fmodules-strict-decluse.
1 | % clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.modulemap -fmodule-name=A a.cc |
Many systems do not ship Clang module map files for C/C++ standard
libraries, so -fmodules-strict-decluse is not suitable.
1
2
3
4
5% clang -fsyntax-only -fmodules-strict-decluse -fmodule-map-file=module.modulemap -fmodule-name=A -fimplicit-module-maps a.cc
a.cc:1:10: error: module A does not depend on a module exporting 'stdio.h'
#include <stdio.h>
^
...
In Bazel, tools/cpp/generate_system_module_map.sh
generates a module map listing all system headers.
-Wprivate-header
In the Clang module
map language, a header with the private specifier cannot be included
from outside the module itself. -Wprivate-header is an
enabled-by-default warning enforcing this rule. The warning is
orthogonal to
-fmodules-decluse/-fmodules-strict-decluse.
To see its effect, change dir/module.modulemap by making
b.h private: 1
2module B { private header "b.h" use C }
module C { header "c.h" }
Then
clang -fsyntax-only -fmodule-map-file=module.modulemap -fmodule-name=A -fimplicit-module-maps a.cc
will report an error: 1
2
3
4a.cc:2:10: error: use of private header from outside its module: 'dir/c.h' [-Wprivate-header]
#include "dir/c.h"
^
1 error generated.
Relation with include-what-you-use and dependency-related linker options
To make full power of the layering check features, the source files must have clean header inclusions.
In the following example, a.cc gets dir/c.h
declarations transitively via dir/b.h but does not include
dir/b.h directly. -fmodules-strict-decluse
cannot flag this case.
1 | cat > a.cc <<'eof' |
If #include "dir/b.h" is added due to clean header
inclusions, -fmodules-decluse will report an error.
In the absence of clean header inclusions, dependency-related
linker options (-z defs,
--no-allow-shlib-undefined, and
--warn-backrefs) can mitigate some brittle build
problems.
With C++20 modules, if B does not export-import
C, a.cc cannot get C's
declarations from import B;.
Build system support
Bazel is the first build system implementing the layering check feature and as of today the only build system.
Bazel
Bazel has implemented the built-in feature
layering_check (https://github.com/bazelbuild/bazel/pull/11440) using
both -fmodules-strict-decluse and
-Wprivate-header.
Bazel generates .cppmap module files from
deps attributes. hdrs and
textual_hdrs files are converted to
textual header declarations while srcs headers
are converted to private textual header declarations.
deps attributes are converted to use declarations.
When building a target with Clang and layering_check is
enabled for the target, Bazel passes a list of
-fmodule-map-file= (according to the build target and its
direct dependencies) and -fmodule-name= to Clang.
1 | touch ./WORKSPACE |
The following build command gives an error with Clang 16. The main
file a.cc includes a.h whose inclusion of
c.h does not have a corresponding use declaration. (Clang
before 16.0 did not check -fmodules-decluse for the
non-main-file textual header a.h: https://reviews.llvm.org/D132779)
1 | % CC=/tmp/Rel/bin/clang bazel build --features=layering_check :a |
The relevant Clang driver options are:
'-fmodule-name=//:a' '-fmodule-map-file=bazel-out/k8-fastbuild/bin/a.cppmap' -fmodules-strict-decluse -Wprivate-header '-fmodule-map-file=external/local_config_cc/module.modulemap' '-fmodule-map-file=bazel-out/k8-fastbuild/bin/b.cppmap'.
Note that c.cppmap is not loaded as :c is not
a direct dependency.
Here are the generated .cppmap module maps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27% cat bazel-out/k8-fastbuild/bin/a.cppmap
module "//:a" {
export *
private textual header "../../../a.h"
use "//:b"
use "@bazel_tools//tools/cpp:malloc"
use "crosstool"
}
extern module "//:b" "../../../bazel-out/k8-fastbuild/bin/b.cppmap"
extern module "@bazel_tools//tools/cpp:malloc" "../../../bazel-out/k8-fastbuild/bin/external/bazel_tools/tools/cpp/malloc.cppmap"
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"
% cat bazel-out/k8-fastbuild/bin/b.cppmap
module "//:b" {
export *
textual header "../../../b.h"
use "//:c"
use "crosstool"
}
extern module "//:c" "../../../bazel-out/k8-fastbuild/bin/c.cppmap"
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"
% cat bazel-out/k8-fastbuild/bin/c.cppmap
module "//:c" {
export *
textual header "../../../c.h"
use "crosstool"
}
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"
external/local_config_cc/module.modulemap contains files
in Clang's default include paths to make
-fmodules-strict-decluse happy.
parse_headers
The feature parse_headers ensures that header files are
self-contained. When this feature is configured and enabled, Bazel
parses non-textual headers in the srcs and
hdrs attributes by compiling them as main files using
-fsyntax-only. These headers do not require other headers
or macros as a precondition. A source file including such a header can
freely reorder it among the included files.
Some problems detected by parse_headers can also be
detected by layering_check. In other words, even if
parse_headers is disabled, layering_check can
still detect these problems.
When layering_check is enabled, it applies to the header
compiles that occur due to parse_headers. Enabling both
features help detect more problems.
Application
llvm-project has an unsupported (in the sence that contributors are
not required to fix it) Bazel build system at utils/bazel/.
features = ["layering_check"] is enabled for llvm/, clang/, and mlir/.
Relation with C++20 modules
Both the module export keyword and the layering check
features can limit the harm of transitive inclusion. The module
export keyword makes a library explicit about what APIs it
exports, while the layering check feature makes the user explicit what
APIs it uses.