Uh oh!
There was an error while loading. Please reload this page.
modules: provide a smallstring C++20 module (and keep the Assert() hook alive) - #12
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
smallstring.hpp had an unconditional `import fmt;`, so the header could not be used at all in a build where fmt is an ordinary library rather than a C++20 module: "fatal error: module 'fmt' not found". Guard it with STDB_USE_FMT_MODULE and fall back to <fmt/format.h>, the same way arena already does.
smallstring.hpp is 5.6k lines and reaches nearly every translation unit in ClapDB through PlainString
(= small::small_byte_string). Textual, every one of them re-parses it.
- smallstring.cppm attaches the header's declarations to module `smallstring`. The header stays the
single source of truth; the .cppm only wraps it.
- smallstring.hpp gets the standard shim: outside the module the include degrades to
`import smallstring;`, so its declarations are never both module-attached and global-module. Gated
on SMALLSTRING_USE_MODULE, so a consumer that does not build with modules is unaffected.
- The Assert() hook survives modularisation. smallstring calls Assert(), not assert(), precisely so
the consumer can substitute its own -- and modules break the old way of doing that: the header is
an `import` at the consumer, and smallstring's macros are fixed when its *interface* is compiled,
not at the include site. So the interface takes a SMALLSTRING_PRELUDE hook; point it at a header
that #define-s Assert. Without it, behaviour is unchanged (plain assert()).
- std comes in as `import std.compat`, NOT as textual libstdc++ headers in the global module
fragment. Textual <stdexcept> there bakes libstdc++'s <string> declarations into the BMI as
global-module entities; any consumer that then reads <string> textually -- directly or through
<fmt/format.h> -- re-declares basic_string.tcc's explicit instantiations on top of them, and clang
rejects it ("explicit instantiation of 'getline' does not refer to a function template"). It
reaches consumers that never name smallstring, too, because a module which imports smallstring
carries those declarations onward in its own BMI. <fmt/format.h> has to stay textual -- the header
specialises fmt::formatter -- and that one is fine.
- The unnamed namespace holding kMinAlignSize / AlignUpTo becomes `small::detail`. Entities in an
unnamed namespace have internal linkage, and a module interface cannot reference an
internal-linkage entity from an exported inline function or template.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:9dfff7ab78
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| auto format(const small::basic_small_string<Char, Buffer, Core, Traits, Allocator, NullTerminated>& str, | ||
| fmt::format_context& ctx) const noexcept { | ||
| return fmt::formatter<std::string_view>::format({str.data(), str.size()}, ctx); | ||
| return fmt::format_to(ctx.out(), "{}", std::string_view{str.data(), str.size()}); |
There was a problem hiding this comment.
Preserve fmt format specifiers for small strings
When the format string contains any non-default presentation, such as fmt::format("{:>5}", small_string("foo")) (covered by regression/string_test.cc:2451), this now formats through a literal {} and the new parse stores no formatter state, so width/alignment/precision are silently discarded and the result becomes foo instead of foo. The previous implementation delegated to fmt::formatter<std::string_view>, so this should keep delegating or store and use an equivalent string-view formatter.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This one was right, and it was a real bug — thanks. It was fixed in 6d0d6b3 (before this thread ever got a reply), and I have just re-verified it independently rather than take the commit's word for it.
The hand-rolled parse() that merely skipped to } stored no state, so the spec was dropped. The formatter now inherits it instead:
structfmt::formatter<small::basic_small_string<...>> : fmt::formatter<std::string_view> {
using fmt::formatter<std::string_view>::parse;
...
};Verified on the textual path (no modules — the path this comment was filed against), compiling straight against smallstring.hpp:
right=' foo' left='foo |' fill='**foo**' prec='fo' plain='foo'
SPEC OK
so width, alignment, fill and precision all survive now, not just the {:>5} case. regression/string_test.cc:2451 (CHECK_EQ(" foo", fmt::format("{:>5}", small_string("foo")))) is the case you pointed at, and it holds.
It holds through the module too, which is the part this PR could have regressed: a consumer that reaches smallstring by import smallstring; gets [ hello] from fmt::format("[{:>8}]", s). The inherited parse() is exported and found across the module boundary, so the specialisation the consumer resolves to is the real one — which is also the evidence I used to answer the newer review on smallstring.cppm.
…dth, fill and alignment
The formatter used to derive from fmt::formatter<std::string_view>, which parses the spec and applies
it. It was replaced by a hand-rolled parse() that just skips to '}' and stores nothing, and a format()
that writes through a literal "{}". Every format spec was therefore discarded in silence:
fmt::format("{:>5}", small_string("foo")) -> "foo" (should be " foo")
fmt::format("{:*^7}", small_string("foo")) -> "foo" (should be "**foo**")
regression/string_test.cc covers this. Delegate to the string_view formatter again.hurricane1026
commented
Jul 12, 2026
Right, and it is worse than a modules nit — thank you. Fixed in The formatter had been changed from deriving
Delegating to the string_view formatter again. |
hurricane1026
commented
Jul 12, 2026
@codex,review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:6d0d6b331b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #if defined(SMALLSTRING_USE_MODULE) && !defined(SMALLSTRING_MODULE_INTERFACE) | ||
| import smallstring; |
There was a problem hiding this comment.
Preserve fmt visibility in module-mode includes
When SMALLSTRING_USE_MODULE is enabled, including this public header expands only to import smallstring;, so it no longer provides the <fmt/format.h> declarations that existing users get from the textual header. The module interface includes fmt only in its global module fragment and does not re-export it, so code such as regression/string_test.cc that includes smallstring.hpp and then calls fmt::format will fail under the new module-mode path unless every caller adds its own fmt include/import.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and reproduced — a TU that includes only smallstring.hpp and calls fmt::format fails under SMALLSTRING_USE_MODULE with clang's own missing '#include ".../fmt/format.h"'; 'format' must be declared before it is used. Fixed in the commit above: the module-mode branch now pulls fmt in exactly the way the textual branch does. It costs nothing, since fmt is textual on both sides of the module boundary (or a module on both sides, under STDB_USE_FMT_MODULE), so the declarations are the same entities either way.
From review. This header has always made <fmt/format.h> visible to whoever includes it, and code leans on that -- regression/string_test.cc includes only smallstring.hpp and then calls fmt::format. Under SMALLSTRING_USE_MODULE the include collapsed to a bare `import smallstring;`, which cannot carry those declarations: fmt sits in the module's global module fragment, and a GMF is not re-exported. So turning the module on silently took fmt away from every consumer. Reproduced with a TU that includes only this header and calls fmt::format: error: missing '#include ".../fmt/format.h"'; 'format' must be declared before it is used The module-mode branch now pulls fmt in exactly the way the textual branch does. That costs nothing -- fmt is textual on both sides of the module boundary (or a module on both sides, under STDB_USE_FMT_MODULE), so the declarations are the same entities either way.
hurricane1026
commented
Jul 12, 2026
@codex,review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:66f00d4118
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // fmt is textual: smallstring.hpp specialises fmt::formatter, and fmt is an ordinary library in this | ||
| // build. (It is the one heavy header that has to stay in the fragment; every other module in the tree | ||
| // carries it in its own fragment too.) | ||
| #include <fmt/format.h> |
There was a problem hiding this comment.
Honor STDB_USE_FMT_MODULE in the module interface
When STDB_USE_FMT_MODULE is enabled, the public shim in include/smallstring.hpp imports fmt as a module before importing smallstring, but this module interface still always pulls in the textual <fmt/format.h>. In that configuration the exported fmt::formatter<small::basic_small_string<...>> specialization is attached to the textual fmt::formatter, while consumers format through the imported fmt::formatter, so fmt::format can fail to see the specialization or hit duplicate fmt declarations. Please mirror the same STDB_USE_FMT_MODULE branch here when building the BMI.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Checked this by building it rather than reasoning about it, and the conclusion is mixed: the diagnosis is wrong for how fmt is actually built here, but it correctly identifies an unstated dependency, so I have written that dependency down and made it self-checking. Fixed in c55555f.
The described failure does not occur. I built fmt's module the way our deps build builds it (deps.cmake.in passes -DFMT_ATTACH_TO_GLOBAL_MODULE whenever STDB_USE_FMT_MODULE is on), built the smallstring BMI with STDB_USE_FMT_MODULE defined, and compiled a consumer that reaches smallstring through the public shim — so import fmt; followed by import smallstring;, precisely the configuration this comment is about — and called fmt::format on a small_string. It compiles, links and runs. fmt::format("[{:>8}]", s) gives [ hello], so the specialisation the consumer resolves to really is the module's (it inherits parse() from formatter<string_view>; a missed specialisation could not have applied the width).
The reason is FMT_ATTACH_TO_GLOBAL_MODULE: it detaches every fmt declaration from module fmt, so the imported fmt::formatter and the textual fmt::formatter are the same global-module entity. That is the macro's stated purpose — fmt's own comment is "you can mix TUs with either importing or #including the {fmt} API". There is no second fmt::formatter for the specialisation to attach to.
The suggested change does not compile, in either fmt flavour. The specialisation derives from fmt::formatter<std::string_view>, and through import fmt; that base resolves to fmt's primary template: no member named 'parse' in 'fmt::formatter<std::basic_string_view<char>>', deleted constructor. This is fmt's behaviour, not smallstring's — a TU containing nothing but import fmt; and a mention of fmt::formatter<std::string_view> fails the same way, while the identical TU with a textual <fmt/format.h> compiles. So the interface unit cannot mirror the shim's branch even if it wanted to.
What the comment is right about is the invariant. I rebuilt fmt's module withoutFMT_ATTACH_TO_GLOBAL_MODULE and the consumer does break — declaration 'basic_appender' attached to named module 'fmt' cannot be attached to other modules, an error pointing into fmt's headers from a TU that need not mention smallstring at all. That configuration is one this project cannot use anyway (without the macro, fmt's symbols get W3fmt module mangling and no textual includer — seastar, most of the tree — can link), but nothing said so. So smallstring.cppm now explains why the include is textual and what it depends on, and #errors if STDB_USE_FMT_MODULE is set while FMT_ATTACH_TO_GLOBAL_MODULE is not, so the failure is named at the point of cause instead of surfacing in someone else's TU. I also corrected the comment in smallstring.hpp, which claimed fmt was "a module on both sides" under STDB_USE_FMT_MODULE — it never was, and that wording is probably what prompted this review.
Default builds are unaffected (STDB_USE_FMT_MODULE is off, the #if is dead); I rebuilt the BMI and libsmallstring_module.a in build.ci to confirm.
…check it
Review asked the module interface to mirror the STDB_USE_FMT_MODULE branch that the public shim
carries -- to `import fmt;` here rather than always include <fmt/format.h> -- on the grounds that
otherwise the exported fmt::formatter<basic_small_string> specialisation attaches to the textual
fmt::formatter while consumers format through the imported one.
That does not happen, and the suggested change does not compile. Built fmt's module the way our deps
build does (FMT_ATTACH_TO_GLOBAL_MODULE), built the smallstring BMI with STDB_USE_FMT_MODULE, and
compiled a consumer that goes through the shim -- so `import fmt;` then `import smallstring;` -- and
calls fmt::format on a small_string: it compiles, links and runs, and `fmt::format("[{:>8}]", s)`
yields "[ hello]", so the specialisation the consumer finds really is the module's, format spec and
all. It works because FMT_ATTACH_TO_GLOBAL_MODULE detaches every fmt declaration from module `fmt`;
imported and textual fmt::formatter are then the same global-module entity, which is the whole point
of that macro.
Doing it the other way round is what breaks. `import fmt;` in this unit fails to build in either fmt
flavour, because the specialisation derives from fmt::formatter<std::string_view> and through an
import that base resolves to fmt's primary template -- deleted constructor, no parse(), no format().
It is fmt's behaviour, not ours: a TU containing nothing but `import fmt;` and a mention of
fmt::formatter<std::string_view> fails identically, while the same TU with a textual <fmt/format.h>
compiles.
The premise is still worth something, though, because it is exactly right about what the arrangement
depends on, and nothing said so. Rebuilding fmt's module without FMT_ATTACH_TO_GLOBAL_MODULE does
break the consumer, with "declaration 'basic_appender' attached to named module 'fmt' cannot be
attached to other modules" -- an error pointing into fmt's headers, from a translation unit that need
not mention smallstring at all. So the condition is now written down where the include is, and an
#error asserts it while the interface is being compiled, where the message can name the cause. The
comment in the shim claimed fmt was "a module on both sides" under STDB_USE_FMT_MODULE; it never was,
and that is corrected too.
Default builds are untouched: STDB_USE_FMT_MODULE is off, the #if is dead, and the include is the
same one it always was.hurricane1026
commented
Jul 12, 2026
@codex,review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:c55555f3bb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #if defined(STDB_USE_FMT_MODULE) | ||
| #ifndef STDB_FMT_IMPORTED | ||
| #define STDB_FMT_IMPORTED 1 | ||
| import fmt; |
There was a problem hiding this comment.
Keep fmt textual when defining the formatter
When STDB_USE_FMT_MODULE is set without SMALLSTRING_USE_MODULE, this path imports fmt and then continues through the textual header, which later defines fmt::formatter<small::basic_small_string...> by deriving from fmt::formatter<std::string_view> at line 5510. The new module unit documents that this same import fmt route resolves the string-view formatter to fmt's primary template and does not compile, so users who only enable the fmt module while keeping smallstring header-only hit that failure. This branch should either keep including <fmt/format.h> textually or reject/route that configuration before the specialization is parsed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right, and a real break — this one I reproduced before touching anything. Fixed in 7d2be69.
Header-only smallstring (no SMALLSTRING_USE_MODULE) with fmt reached by import, exactly as described:
smallstring.hpp:5515: error: no member named 'parse' in 'fmt::formatter<std::basic_string_view<char>>'
smallstring.hpp:5519: error: no member named 'format' in 'fmt::formatter<std::basic_string_view<char>>'
Which is the failure the new module unit documents, arriving from the other side: fmt's formatter<std::string_view> specialisation is not reachable through import fmt;, so the base resolves to the primary template. The textual branch is the one that carries the body, so it is the one that defines the specialisation and needs that base to be real. It now always includes <fmt/format.h>.
Worth being precise about why the other branch keeps its import fmt;, since the two look symmetric and are not. Under SMALLSTRING_USE_MODULE the body is never parsed — the header becomes import smallstring;, the specialisation arrives ready-made from the BMI, and all the consumer needs from fmt is its declarations in order to call fmt::format. There is no base class to resolve, so nothing to break. I checked that distinction rather than assumed it: the module consumer compiles, links and runs with the spec applied across the boundary (fmt::format("[{:>8}]", s) → [ hello]).
After the fix, all the header-only combinations build:
| result | |
|---|---|
| header-only + textual fmt (default) | ok |
| header-only + fmt as a module | ok (was the break) |
| header-only + fmt as a module, format specs | ' foo' `'foo |
and the module path plus the FMT_ATTACH_TO_GLOBAL_MODULE guard still behave as before. build.ci's default build (fmt module off) rebuilt clean.
… module
The textual branch of the shim imported fmt whenever STDB_USE_FMT_MODULE was set. That branch is the
one that carries the body, so it is the one that *defines* fmt::formatter<basic_small_string>, and the
definition derives from fmt::formatter<std::string_view>. Through an import that base is fmt's primary
template, so the header simply does not compile: "no member named 'parse' in
'fmt::formatter<std::basic_string_view<char>>'", deleted constructor. Anyone who turned the fmt module
on while keeping smallstring header-only hit it. Reproduced exactly that way -- header-only
smallstring, fmt reached by import -- and the two errors land on the formatter's parse and format.
It is the same failure the module interface already documents, arriving from the other side, and the
reason is the same: fmt's formatter<std::string_view> specialisation is not reachable through
`import fmt;`. So the fix is the same as there. This branch now always includes <fmt/format.h>.
The import stays in the SMALLSTRING_USE_MODULE branch, where it is correct and necessary: that branch
never parses this body, the specialisation reaches the consumer ready-made from the BMI, and all the
consumer needs from fmt is its declarations. Verified that distinction rather than assumed it --
header-only + fmt module now compiles and still honours the spec ("{:>5}" on a small_string gives
" foo", and fill, alignment and precision with it), while the module consumer continues to compile,
link and run with the format spec applied across the module boundary.hurricane1026
commented
Jul 12, 2026
@codex, review |
The last `import fmt;` was in the module-consumer branch of the shim. It worked, but it should not
have existed, and keeping it was what kept generating bugs: of the three real problems found in this
PR, two were in an fmt-import path (the interface unit, then the header-only path), and both had the
same cause.
seastar decides the question. ~21 of its headers reach fmt through <fmt/format.h> and will keep doing
so, so fmt -- if it is ever built as a module here at all -- has to carry FMT_ATTACH_TO_GLOBAL_MODULE
or the tree does not link. That macro leaves fmt's declarations attached to the global module, which
is exactly the condition under which a textual read of fmt meets an imported one. So smallstring can
simply always read fmt textually, in every configuration, and be correct whether or not anything else
in the program imports it.
The import bought nothing to weigh against that. Any TU that pulls in seastar already has fmt
textually, so there was no parse to save; and it cannot be made to work in the branch that defines
fmt::formatter<basic_small_string>, because through an import the fmt::formatter<std::string_view> it
derives from resolves to fmt's primary template.
So the three-state shim no longer branches on how fmt is consumed at all: <fmt/format.h>, always. What
remains of STDB_USE_FMT_MODULE is a single assertion at the top of the header -- if fmt is a module
and is not attached to the global module, say so there, by name, instead of letting it surface as
"declaration 'basic_appender' attached to named module 'fmt' cannot be attached to other modules" in
somebody's unrelated translation unit.
Checked all four ways in: header-only with textual fmt, header-only with fmt as a module, module
consumer with fmt as a module (compiles, links, runs, "{:>8}" still lands as "[ hello]" across the
boundary), and the assertion firing on the one combination that cannot work. Format specs still hold
on the textual path -- width, alignment, fill, precision. build.ci's default build is unchanged.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:39a56469a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #else | ||
| #ifndef SMALLSTRING_MODULE_INTERFACE |
There was a problem hiding this comment.
Keep assert visible in the module interface
When building smallstring.cppm without SMALLSTRING_PRELUDE, this new guard suppresses the header's <cassert> include, but the body still defines the fallback Assert macro as assert(...) and expands it throughout the exported inline/template definitions. import std.compat does not import the assert macro, so the default module-interface build depends on an unrelated header having defined it and otherwise fails to compile. Please include <cassert> in the module interface/global fragment or special-case the fallback before exporting the header body.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right, and it does not compile — reproduced before changing anything. Fixed in ddac83d.
Precompiling smallstring.cppm with SMALLSTRING_PRELUDE left undefined:
smallstring.hpp:602:17: error: use of undeclared identifier 'assert'
smallstring.hpp:1013:13: error: use of undeclared identifier 'assert'
smallstring.hpp:1017:13: error: use of undeclared identifier 'assert'
smallstring.hpp:1043:17: error: use of undeclared identifier 'assert'
...
one per Assert() call site, exactly as you describe — and import std.compat cannot cover for it, since assert is a macro and macros do not cross a module boundary. Only the prelude was hiding this: clapdb always points SMALLSTRING_PRELUDE at a header that defines Assert, so the fallback never fires there. Anyone building the module the way this file's own comment describes — "without it, smallstring.hpp falls back to plain assert() as before" — got a module that does not build.
On placement, I went with the global module fragment rather than the header. Of your two suggestions only that one works: on the module path the header body is read from inside an export { } block, where a first-time #include is not allowed — which is precisely why the include block is guarded out in the first place. So the fallback's dependency now sits in smallstring.cppm, next to the SMALLSTRING_PRELUDE hook it backs up. It costs importers nothing (a macro expanded while the interface is preprocessed reaches no one), and <cassert> is just <assert.h>, so it carries none of the libstdc++ declarations the neighbouring note warns about.
I also checked the fix restores working assertions rather than merely silencing the error, which a stub #define assert would also have done: a consumer built against a no-prelude BMI compiles, links and runs (hello len=5 fmt=[ hello]), and its object file carries a real __assert_fail reference. The prelude path and the STDB_USE_FMT_MODULE path both still build and pass their probes.
…h was missing it
Building smallstring.cppm without SMALLSTRING_PRELUDE does not compile. The header's `#ifndef Assert`
fallback expands to assert(), but the module-interface path skips the whole include block that would
have brought <cassert> in, so every call site fails with "use of undeclared identifier 'assert'".
`import std.compat` cannot cover for it: assert is a macro, and macros do not cross a module boundary.
Reproduced by precompiling the interface with the prelude left undefined -- the errors land one per
Assert(), at smallstring.hpp:602, :1013, :1017, :1043 and on.
Only the prelude hides it, which is why it survived this long: clapdb always points SMALLSTRING_PRELUDE
at a header that defines Assert, so the fallback never fires there. Anyone building the module the way
the comment in this file describes -- "without it, smallstring.hpp falls back to plain assert() as
before" -- got a module that does not build.
The include has to go here, not in the header. On the module path the header body is read from inside an
`export { }` block, and a first-time #include is not allowed there; that is exactly why the include block
is guarded out. So the fallback's dependency belongs in the global module fragment, next to the
SMALLSTRING_PRELUDE hook it backs up. It costs nothing: assert is a macro expanded while this unit is
preprocessed, so it reaches no importer, and <cassert> is just <assert.h> -- none of the libstdc++
declarations the note below it warns about.
Checked that this actually restores assertions rather than just silencing the error: a consumer built
against a no-prelude BMI compiles, links, runs, and its object carries a real __assert_fail reference.
The prelude path and the STDB_USE_FMT_MODULE path are both unchanged.hurricane1026
commented
Jul 12, 2026
@codex,review |
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Uh oh!
There was an error while loading. Please reload this page.
smallstring.hpp is 5.6k lines and, through
PlainString, reaches nearly every translation unit in ClapDB. Textual, every one of them re-parses it. This makes it a C++20 module.What is here
smallstring.cppmattaches the header's declarations to modulesmallstring. The header stays the single source of truth; the .cppm only wraps it.smallstring.hppgets a shim: outside the module the include degrades toimport smallstring;, so its declarations are never both module-attached and global-module. Gated onSMALLSTRING_USE_MODULE, so a consumer that does not build with modules is completely unaffected.Assert()hook survives. smallstring callsAssert(), notassert(), precisely so a consumer can substitute its own. Modules break the old way of doing that — the header is animportat the consumer, and smallstring's macros are fixed when its interface is compiled, not at the include site. So the interface takes aSMALLSTRING_PRELUDEhook: point it at a header that#definesAssert. Without it, behaviour is unchanged (plainassert()).kMinAlignSize/AlignUpTobecomessmall::detail. Entities in an unnamed namespace have internal linkage, and a module interface cannot reference an internal-linkage entity from an exported inline function or template.import fmt;is guarded. It was unconditional, so the header could not be used at all in a build where fmt is an ordinary library rather than a C++20 module (fatal error: module 'fmt' not found).One module-shape detail worth knowing
The interface takes std through
import std.compat, not textual libstdc++ headers in its global module fragment. Textual<stdexcept>there bakes libstdc++'s<string>declarations into the BMI as global-module entities, and any consumer that then reads<string>textually — directly, or through<fmt/format.h>— re-declaresbasic_string.tcc's explicit instantiations on top of them:It reaches consumers that never name smallstring, too, because a module that imports smallstring carries those declarations onward in its own BMI. 22 TUs in ClapDB failed exactly this way before the switch.
<fmt/format.h>has to stay textual — the header specialisesfmt::formatter— and that one is fine.Validation
Building all of ClapDB against this: fresh 64-core build 6m44s → 6m23s, and
alltestsis 7718/7718 cases / ~1.5M assertions green at 8 and at 128 shards.