rfcs/proposed/cxx20_modules/README.md
C++20 introduced modules as a modern alternative to the traditional header-based inclusion model. Modules offer several advantages over headers:
As the C++ ecosystem gradually adopts modules, oneTBB should provide a module interface to
allow users to use the library using import tbb; instead of #include <oneapi/tbb.h>.
The proposed approach is to provide a wrapper module that includes the existing headers in the global module fragment and re-exports public symbols as suggested in LLVM documentation.
The module interface unit (e.g., tbb.cppm) would follow this structure:
module;
// Global module fragment — include all public headers
#include <oneapi/tbb.h>
export module tbb;
// Re-export public API via using-declarations
export namespace tbb {
// Parallel algorithms
using tbb::parallel_for;
using tbb::parallel_reduce;
using tbb::parallel_scan;
using tbb::parallel_sort;
using tbb::parallel_invoke;
using tbb::parallel_pipeline;
using tbb::parallel_for_each;
// Flow Graph API
namespace flow {
using tbb::flow::graph;
// ... other flow graph API
}
// other TBB API
}
Consumer code uses the module as follows:
import tbb;
int main() {
// oneapi:: namespace is also available
tbb::parallel_for(0, 100, [](int i) {
// Nested namespaces are available as well
tbb::this_task_arena::isolate([] {
// ... do some work in isolation
});
});
}
The proposed approach has several advantages over other alternatives:
using
declarations. Thus, development can continue in headers.The disadvantage of this approach is an obligation to update the module interface unit each time new API is added.
The project policy requires C++11 as the minimum standard, hence the module source cannot
be compiled as part of the oneTBB build because it would force C++20 standard on the library.
With the ABI non-breaking approach, the library built with C++11 can be used together with
the module interface unit compiled with C++20 or higher. Furthermore, a prebuilt module constrains
the consumer to the exact C++ standard version used during the module build. Thus, the .cppm file
is installed as a source file and compiled on the consumer side.
find_package(TBB REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE TBB::tbb)
# tbb.cppm file is located under include/oneapi
get_target_property(_tbb_include_dir TBB::tbb INTERFACE_INCLUDE_DIRECTORIES)
target_sources(myapp PRIVATE
FILE_SET cxx_modules TYPE CXX_MODULES
BASE_DIRS ${_tbb_include_dir}
FILES ${_tbb_include_dir}/oneapi/tbb.cppm
)
In the future, TBBConfig.cmake could provide a helper function (e.g.,
tbb_enable_cxx20_modules(myapp)) to reduce this boilerplate.
The consumer compiles the .cppm as part of their own target with their own flags. Name
mangling is unchanged, so the consumer links against the same libtbb.so / tbb.lib
regardless of whether they use modules or headers.
Module-based API availability is tested by extending test_tbb_header. A C++20 module interface
unit (test_tbb_header_module.cppm) either includes TBB headers in its global module fragment
or imports the tbb module, then exports the test code. A consumer imports this module to verify
public API accessibility. An ABI compatibility check, in particular, combining
module and header based TBB usage across multiple translation units, can also be performed.
Running the whole TBB test suite with import tbb; seems to provide little value, since the module
unit is implemented as a wrapper around headers. It may help catch some missing exported API, but
implementing and maintaining such an "import" mode can be challenging and costly. Hence,
the proposed testing approach seems to be a good balance between coverage and maintainability.
Instead of re-exporting symbols via using-declarations, library headers are included
inside an extern "C++" language linkage block within the module purview, and all
third-party/system headers are included in the global module fragment.
module;
// Global module fragment — system and third-party headers only
#include <cstddef>
#include <atomic>
#include <functional>
// ... all standard/third-party headers used by TBB
export module tbb;
#define __TBB_IN_MODULE_USE
extern "C++" {
#include "oneapi/tbb.h"
}
Declarations within an extern "C++" block inside the module purview are actually attached to
the global module fragment by the standard. An important detail here is that TBB headers will need
to define some macro (e.g. __TBB_CXX20_EXPORT) if __TBB_IN_MODULE_USE is defined. The definition
of __TBB_IN_MODULE_USE would also mean that all inclusions of third-party headers should be
excluded from TBB headers.
Pros:
using-declarations for every public symbol.__TBB_CXX20_EXPORT to the declaration.Cons:
export keyword to public
declarations and stripping the inclusion of third-party headers from them.Same header-inclusion approach, but library headers are included directly in the module
purview without an extern "C++" block. This causes the module name to participate in
symbol mangling.
module;
// Global module fragment — system and third-party headers only
#include <cstddef>
#include <atomic>
#include <functional>
// ... all standard/third-party headers used by TBB
export module tbb;
#define __TBB_IN_MODULE_USE
#include <oneapi/tbb.h>
Pros:
using-declarations for every public symbol.__TBB_CXX20_EXPORT to the declaration.Cons:
Besides the single tbb module, should fine-grained submodules
(e.g., tbb.flow_graph, tbb.containers) be provided so that consumers can precompile and
import only what they need?
How to provide preview functionality with modules? TBB preview features are currently gated by
TBB_PREVIEW_* macros that are defined before including headers. Macros defined by the consumer
before import tbb; cannot influence the module's already-compiled interface. One option is to
provide a separate tbb.preview module. However, some preview features are extensions to
existing classes and exporting them would create distinct types in a tbb.preview module, which
raises questions about whether tbb and tbb.preview modules can be used together.
tbb.cppm with desired
TBB_PREVIEW_* macros defined.How to support public macros such as TBB_VERSION or feature-test macros?
Modules do not export preprocessor definitions, so these macros are not visible
after import tbb;. Some options:
#include <tbb/version.h> alongside the import.Should the module be split into partitions (e.g., tbb:algorithms, tbb:containers,
tbb:flow_graph) to organize the using-declarations?
Partitions are internal to the module and not importable by consumers, but could
improve maintainability of the module interface.
Currently there is a limitation with transitive inclusion of the STL headers. Certain combinations of standard headers inclusions and C++23 standard module imports can cause compilation errors. It in unclear whether this is a compiler limitation or a limitation of modules in general. Regardless, the question is how to address this issue (e.g. allow the consumer to switch between includes and imports in TBB module unit) and how should the testing suite be extended to cover these problematic scenarios.
The following conditions should be met before this feature graduates from experimental to fully supported: