· 5 min read
The include you never wrote
libc++ 23 is removing incidental transitive includes, turning a class of mysteriously successful C++ builds into small, useful compiler errors.

Here is a C++ file with a dependency missing from the top of it. Depending on your standard library and version, it may still compile. That is the annoying part.
#include <vector>int main() { std::vector<int> values{3, 1, 2}; std::sort(values.begin(), values.end());}std::sort belongs to <algorithm>, but older libc++ releases could make the name visible while this file included only <vector>. The source borrowed an include from the implementation details of another header and never wrote the debt down.
The current libc++ 23 release notes warn that a large set of those incidental transitive includes has been dropped in every language mode. Some builds will fail after the toolchain update. I think those failures are doing useful work: they point at dependencies the source already had and finally make the file admit them.
The old dependency is still in the header#
The nicest piece of evidence is sitting in libc++ itself. On the 23 release branch, the public vector header contains a compatibility block that can put the old incidental includes back. For C++20 and earlier, the list includes <algorithm> alongside a surprising amount of unrelated library surface.
#if defined(_LIBCPP_KEEP_TRANSITIVE_INCLUDES_LLVM23) && _LIBCPP_STD_VER <= 20# include <algorithm># include <atomic># include <cctype># include <concepts># include <cstdint># include <iosfwd># include <optional># include <string># include <string_view># include <tuple># include <type_traits># include <utility>#endif- 1The compatibility macro is an escape hatch for code that depended on LLVM 22's include graph.
- 2This is why a file using
std::sortcould get away without naming<algorithm>. - 3
<vector>also happened to expose names from<optional>, strings, tuples, traits, and other headers the vector synopsis does not promise.
That list is a useful reminder that transitive inclusion is not one neat chain. A public header includes private implementation headers, those need utilities, the utilities need traits, and soon a translation unit can see declarations that arrived through a path nobody writing the file intended. Change one internal edge and some unrelated source file stops finding a name.
libc++ has been trying to reduce this for years. Its header-removal policy names the tradeoff plainly: extra includes cost compile time and create accidental dependencies, but removing them can break users and leave toolchain vendors repairing upstream packages. Earlier cleanups were often tied to a new C++ language mode so that projects crossed the break deliberately. LLVM 23 is a broader cleanup, which is why it ships a compatibility macro instead.
A compiler error is cheaper than a hidden edge#
The repair for the example is one line.
#include <algorithm>#include <vector>int main() { std::vector<int> values{3, 1, 2}; std::sort(values.begin(), values.end());}Nothing about the program's behavior changed. The useful change is that a reader can now see why std::sort is available, and the file no longer depends on whether <vector>happens to need an algorithm internally this year. The same source is less coupled to libc++'s private include graph, and usually more portable across standard-library implementations too.
I would not turn this into a contest for the fewest possible includes. The C++ standard sometimes requires one public header to provide declarations from another, and implementations still need internal layering. The useful rule is narrower: when your source directly names a facility, include the public header that owns that facility instead of relying on a neighboring header to drag it in.
You can find the debt before the upgrade#
libc++ exposes _LIBCPP_REMOVE_TRANSITIVE_INCLUDES for exactly this kind of testing. Define it and the library follows a stricter include policy even before a language-version transition. Its user documentation also ships a mapping file for include-what-you-use, because automated include advice gets much better when the tool understands libc++'s public and private header boundaries.
There is a cost here. Large C++ trees can discover hundreds of missing direct includes at once, and distro maintainers are the people who get the first unpleasant morning when a new standard library reaches their builders. That is why the 23 release notes keep _LIBCPP_KEEP_TRANSITIVE_INCLUDES_LLVM23 around as a migration switch rather than pretending every downstream project can be fixed in one pass. The same notes say the switch goes away in LLVM 24.
So I would use the escape hatch to keep a release moving, then remove it by fixing the source. A build that breaks on std::sort after an upgrade looks like the library took something away. In this case the compiler is showing you a line the file should have contained all along: #include <algorithm>.