diff --git a/include/smallstring.hpp b/include/smallstring.hpp index 48020e4..0a0ecdf 100644 --- a/include/smallstring.hpp +++ b/include/smallstring.hpp @@ -13,7 +13,52 @@ */ #pragma once +// smallstring reads fmt textually, always -- header-only, module consumer, module interface alike. It +// never imports fmt, in any configuration. +// +// It can afford to be that blunt because fmt, if it is a module at all, has to be built with +// FMT_ATTACH_TO_GLOBAL_MODULE: seastar reaches fmt through in ~21 of its headers and +// will keep doing so, so fmt's declarations must stay attached to the global module or nothing in the +// tree links. That macro is precisely what makes textual and imported fmt the *same* entities ("you +// can mix TUs with either importing or #including the {fmt} API" -- fmt's own words), so a textual +// read here meets an imported fmt anywhere else in the program. +// +// The converse does not hold, which is why there is no `import fmt;` branch to balance this one: the +// fmt::formatter specialisation below derives from fmt::formatter, +// and through an import that base resolves to fmt's *primary* template -- "no member named 'parse'", +// deleted constructor. That is fmt's behaviour, not smallstring's (a TU that does nothing but +// `import fmt;` and name fmt::formatter fails identically), and it is why every +// attempt to route this header's fmt through the module has been a bug. +#if defined(STDB_USE_FMT_MODULE) && !defined(FMT_ATTACH_TO_GLOBAL_MODULE) +#error "smallstring reads fmt textually and specialises fmt::formatter, so an fmt built as a C++20 module must be built with FMT_ATTACH_TO_GLOBAL_MODULE -- otherwise its declarations attach to module `fmt` and the textual ones here cannot match them." +#endif + +// Owned by module `smallstring` when the consumer builds with modules (SMALLSTRING_USE_MODULE). +// +// Outside that module the include degrades to the import, so these declarations are not ALSO +// re-declared in the global module. A header is textual everywhere or module-owned everywhere, never +// both: mixing the two gives every type here two definitions, and nothing links. +// +// `import` is legal in a global module fragment, so a .cppm that reaches this header from its GMF is +// fine. What is NOT fine is reaching it from inside an `export { }` block for the first time -- put it +// in that module's GMF instead. +#if defined(SMALLSTRING_USE_MODULE) && !defined(SMALLSTRING_MODULE_INTERFACE) + +// fmt as well, not just the import. This header has always made visible to whoever +// includes it, and plenty of code leans on that -- regression/string_test.cc includes only +// smallstring.hpp and then calls fmt::format. The module cannot carry those declarations across: fmt +// sits in its global module fragment and a GMF is not re-exported, so `import smallstring;` alone would +// silently take fmt away from every consumer the moment SMALLSTRING_USE_MODULE is turned on. +// +// Keeping the include here costs nothing: it is the same the interface unit read, and +// under FMT_ATTACH_TO_GLOBAL_MODULE (see the top of this file) the same entities either way. #include + +import smallstring; + +#else + +#ifndef SMALLSTRING_MODULE_INTERFACE #include #include @@ -31,11 +76,20 @@ #include #include +// This is the branch that carries the body, so it is the one that *defines* the formatter +// specialisation -- the case the note at the top of this file is really about. Textual, always. +#include + +#endif // !SMALLSTRING_MODULE_INTERFACE + namespace small { #ifndef Assert #define Assert(condition, message) assert((condition) && (message)) #endif -namespace { +// Not an unnamed namespace: entities there have internal linkage, and a C++20 module interface +// cannot reference an internal-linkage entity from an exported inline function or template +// ("'kMinAlignSize' has internal linkage and cannot be referenced from an exported ..."). +namespace detail { inline constexpr uint64_t kMinAlignSize = 8; // 64 bits for modern cpu /** * @brief Aligns a value up to the next multiple of N @@ -54,7 +108,10 @@ template return (n + N - 1) & static_cast(-N); } -} // namespace +} // namespace detail + +using detail::AlignUpTo; +using detail::kMinAlignSize; /** * @brief Storage strategy enumeration for small string optimization @@ -5452,6 +5509,9 @@ template > : fmt::formatter { + // Delegate to the string_view formatter, which parses and applies the format spec. A hand-rolled + // parse() that merely skips to '}' stores no state, so width, alignment, fill and precision are + // silently discarded -- fmt::format("{:>5}", small_string("foo")) would give "foo", not " foo". using fmt::formatter::parse; auto format(const small::basic_small_string& str, @@ -5651,3 +5711,5 @@ struct hash -- which is why this include is here rather than in the header. +// +// smallstring.hpp does include , but only on its textual path: the module-interface path skips +// the whole include block, and it has to, because it is read from inside an `export { }` block where a +// first-time #include is not allowed. Its `#ifndef Assert` fallback still expands to assert(), though, +// so with no SMALLSTRING_PRELUDE to define Assert the interface would not compile -- "use of undeclared +// identifier 'assert'", once per call site. `import std.compat` cannot rescue it either: assert is a +// macro, and macros do not cross a module boundary. +// +// So the fallback's dependency belongs here, next to the hook it backs. It is a macro, expanded while +// this unit is preprocessed, so nothing about it reaches importers; is just , and +// carries none of the libstdc++ declarations the note below is about. +#include + +// std comes in as `import std.compat` below, NOT as textual libstdc++ headers here. +// +// Textual in this fragment would bake libstdc++'s declarations into the BMI as +// global-module entities. Any consumer that then reads textually -- directly, or through +// -- 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 that imports smallstring carries those +// declarations onward in its own BMI. +#include + +// fmt is textual, here and everywhere else in smallstring -- the header we are about to read +// specialises fmt::formatter, and it is the one heavy header that has to stay in this fragment. The +// note at the top of include/smallstring.hpp explains why it is never an `import fmt;`, and asserts +// the FMT_ATTACH_TO_GLOBAL_MODULE that lets a textual read here meet an imported fmt elsewhere. +#include + +export module smallstring; + +import std.compat; + +#define SMALLSTRING_MODULE_INTERFACE 1 +export { +#include "smallstring.hpp" +} +#undef SMALLSTRING_MODULE_INTERFACE