This article describes some Clang header modules features which apply to #include
. They enforce a more explicit dependency graph. Strict dependency information provides documentation purposes and makes refactoring convenient. Include What You Use describes the benefits of clean header inclusions well, so I will not repeat it here.
When C++20 modules are used, the features apply to #include
in a global module fragment (module;
) but have no effect for import declarations.
-fmodules-decluse
For a #include
directive, this option emits an error if the following conditions are satisfied (see clang/lib/Lex/ModuleMap.cpp
diagnoseHeaderInclusion
):
A
).B
.A
does not have a use-declaration of B
(no use 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.
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;
.
Bazel is the first build system implementing the layering check feature and as of today the only build system.
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: a.h
's inclusion of c.h
does not have a corresponding use declaration. (Clang before 16.0 did not check -fmodules-decluse
in textual headers: 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 header files are self-contained. When the feature is configured and enabled, Bazel parses non-textual headers in srcs
and hdrs
attributes by compiling them as main files using -fsyntax-only
. Such headers do not require other headers or macros as a precondition. A source file including the such a header can freely reorder it among included files.
Some problems detected by parse_headers
can be detected by layering_check
as well. I.e. even if parse_headers
is disabled, layering_check
can detect the problems.
When layering_check
is enabled, it applies to the header compiles due to parse_headers
. Enabling both can detect more problems.