· 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.

Rows of library books sit on low wooden shelves, each volume carrying its own spine label while more books are displayed on top.
Mshuang2, CC0 1.0

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.

sort.cppcpp
#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.

libcxx/include/vector, compatibility excerptcpp
#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
  1. 1The compatibility macro is an escape hatch for code that depended on LLVM 22's include graph.
  2. 2This is why a file using std::sort could get away without naming <algorithm>.
  3. 3<vector> also happened to expose names from <optional>, strings, tuples, traits, and other headers the vector synopsis does not promise.
The compatibility block records part of the old accidental API of <vector>.

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.

sort.cpp, with its own dependencycpp
#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>.