The C++ Standard Library Has Been Walking Itself Back for Fifteen Years, and the Receipts Are Public
Pangram verdict · v3.3
We believe that this document is primarily AI-generated with some human-written content
AI likelihood · overall
AIArticle text · 1,841 words · 6 segments analyzed
The C++ standard library has been walking itself back for fifteen years, and the receipts are public Sandor Dargo's post this month on std::copyable_function closes with a quick-reference table. Four callable wrappers, one recommendation each, and at the bottom of the list one entry that should stop any working C++ engineer cold:
std::function: Legacy. Avoid in new code.
std::function shipped in C++11. The committee spent fifteen years shipping the wrappers that should replace it. The latest, std::copyable_function, lands in C++26. The recommendation written on top of the new arrival is not "use this when you need a copyable callable." It is "do not use the original." This is not unusual. The C++ committee has been writing that sentence about its own features since C++11 was new. Sometimes the sentence is formal (a paper number, a deprecation in the standard, a removal one cycle later). Sometimes the sentence is what every senior engineer tells every junior engineer on day one ("never reach for that, here is what to use instead"). And sometimes the sentence cannot be written into the standard at all, because the broken thing is locked in by ABI compatibility, so it stays in the standard library as the default that every tutorial reaches for and every production codebase quietly replaces. The pattern is so consistent that it deserves its own catalogue, with paper numbers next to every entry, so the next time someone tells you the new C++ feature is the future you can ask them to estimate how long until the next paper deprecates it. This piece is that catalogue, in three tiers. The first tier is the formal walk-backs the committee has written down. The second tier is the "everyone knows to avoid this" walk-backs that the committee has not formalised. The third tier is the most damning, because it is the standard library containers that almost every C++ codebase uses every day and that the committee cannot fix without breaking ABI. We have receipts on the third tier from our own Rust-vs-C++ multi-book benchmark, which measured 58 times the P99 latency between Rust's standard library and C++'s on identical workloads with identical isolation, and which traced the gap to three containers the committee has never formally said are broken.
Tier 1: The formal walk-backs the committee has written down Every entry below points at a real paper that the working group adopted. None of these are arguments. They are admissions in writing. The cleanest historical case is std::auto_ptr, the C++98 smart pointer whose copy-as-move semantics broke generic code and standard containers from the day it shipped. Deprecated in C++11, removed in C++17 by N4190 "Removing auto_ptr, random_shuffle(), And Old <functional> Stuff", Stephan T. Lavavej. That single paper also took out the entire <functional> adapter zoo from C++98: std::bind1st, std::bind2nd, std::ptr_fun, std::mem_fun, std::mem_fun_ref, std::unary_function, std::binary_function, std::pointer_to_unary_function, std::pointer_to_binary_function. The replacement is the lambda, a language feature that landed two cycles earlier and made the entire adapter framework irrelevant. std::random_shuffle went with them, deprecated in C++14, removed in C++17, replaced by std::shuffle because the original depended on std::rand and global state. Dynamic exception specifications (throw(X, Y)) were the C++98 mechanism for declaring which exceptions a function could throw. Deprecated in C++11, removed in C++17 by P0003R5, Alisdair Meredith. Replacement: noexcept. The vestigial throw() synonym for noexcept(true) survived until C++20, when P1152 finally killed it. Eighteen years of standard text spent un-shipping an exception model. std::iterator, the C++98 base class every "Effective C++" book taught you to inherit from, was deprecated in C++17 by P0174R2 ("Deprecating Vestigial Library Parts in C++17", Meredith). Removal is now proposed for C++26 in P3365R1. The replacement is "define the five typedefs yourself", which is what most engineers were doing anyway because inheriting from std::iterator never gave you anything useful.
std::aligned_storage and std::aligned_union shipped in C++11, were deprecated in C++23 by P1413R3 (CJ Johnson, Google). The paper's rationale is worth quoting because it captures the committee admitting a design error on its own work. The deprecated types require typename ::type boilerplate, require reinterpret_cast to access the contents, treat Len == 0 as undefined behaviour, and are not constexpr. The replacement is "use alignas(T) std::byte[sizeof(T)] directly", which is what the standard probably should have shipped in the first place. std::not1/std::not2 and the unary_negate/binary_negate adapters were deprecated in C++17, removed in C++20, replaced by std::not_fn (P0005). std::get_temporary_buffer and std::raw_storage_iterator were deprecated in C++17 by P0174 and removed in C++20 by P0619. The register keyword was deprecated in C++11 and removed in C++17 by P0001. Trigraphs were removed in C++17 after thirty years of being a wart on the language. The most embarrassing entry in the formal-walk-back catalogue is the C++11 garbage collection interface. The committee shipped std::declare_reachable and friends in C++11. No major implementation ever provided a real garbage collector behind those entry points. The interface was removed in C++23 by P2186R2 (JF Bastien), having been added and removed without ever once functioning as advertised. Twelve years of standard text spent un-shipping a feature nobody used. Then there are the Technical Specification rollbacks, the parts of the standardisation pipeline that the committee outright rejected before merging.
The Concepts TS was redesigned for C++20 (P0734 family). The Modules TS was redesigned via the merged-modules proposal P1103R3. The Coroutines TS was substantially modified before adoption. The Reflection TS was rejected entirely and replaced by P2996 for C++26, a completely different value-based design. The Executors TS went through multiple rejected rounds before becoming P2300 sender/receiver in C++26. The Networking TS has been deferred so many times that as of C++26 it is still not in the standard. Every one of those is the same admission written differently: "the previous design did not work, here is the next one." And finally the trigger of this whole article. std::function shipped in C++11. Its const operator() invokes non-const callables, which is a const-correctness defect that has been part of the standard for fifteen years and cannot be fixed without breaking ABI. The committee's response has been to ship a sequence of replacements in adjacent cycles: std::move_only_function in C++23 via P0288R9, std::copyable_function in C++26 via P2548R6, and std::function_ref in C++26 via P0792R14. The original std::function is still in the standard. Dargo's table tells you to avoid it. So does every working C++ codebase you can audit. Tier 2: The "everyone knows to avoid this" walk-backs the committee has not written down These features are still in the standard. None of them are formally deprecated. Every senior C++ engineer in the industry will tell every junior engineer to avoid them on the first day of the job. std::regex shipped in C++11. The committee's own paper P1844R1 records, in writing, that "the C++ committee noted that std::regex performance is very poor relative to other available solutions" and discouraged spending implementation effort on it. The standard library shipped a feature whose primary documented quality is that the committee acknowledges it is too slow to use. The replacement is Hana Dusíková's CTRE (compile-time regular expressions), with a standardisation attempt in P1433R0.
Outside the standard the replacement is Boost.Regex, RE2, or PCRE2. Production code uses one of those. The standard std::regex exists for tutorials. std::async shipped in C++11. The destructor of the returned future blocks until the async operation completes. N3679 documents the resulting deadlock trap. The replacement is the entire sender/receiver effort that finally landed in C++26 via P2300, fifteen years after std::async first shipped broken. In the meantime, every working low-latency codebase uses thread pools, std::thread directly, or platform-specific async primitives. std::async exists in the standard so that introductory textbooks have something to write about. <iostream> shipped in 1998. It is slow, locale-bound, thread-unsafe for formatting, and produces error messages that are widely considered a hazing ritual for new C++ engineers. The committee shipped P0645 std::format in C++20 and P2093 std::print / std::println in C++23. Neither deprecates <iostream>. The committee will not write that sentence. It is also the sentence every working engineer is told as soon as they ship printf-style debug output in a code review. std::list is the canonical entry in this tier. Bjarne Stroustrup spent the 2012 GoingNative keynote showing that std::vector beats std::list even for the textbook "insertion in the middle of a large container" workload, because the linear scan dominates and the pointer chase punishes the cache. The follow-up post is titled, with deliberate emphasis, Are lists evil?. The answer is yes. std::list is not deprecated. It exists in the standard. Every working C++ engineer is told never to reach for it. std::deque is the next entry. The Microsoft STL maintainers have a public issue, microsoft/STL#147, titled "<deque>: Needs a major performance overhaul", acknowledging that the standard's mandated block size is too small and the design needs to be rebuilt at the next ABI break. Until then, std::deque ships in every standard library with the same poor cache behaviour everyone has known about for twenty years. std::valarray shipped in 1998 as a numeric container with expression-template optimisation potential.
The optimisation work was never done. The current cppreference text says implementations "don't appear to have any special code" beyond a plain container. Eigen, xtensor, and Blaze fill the niche. std::valarray is in the standard for archaeological reasons. std::vector<bool> is the famous case. Howard Hinnant's On vector<bool> is the canonical analysis. The bit-packed storage is genuinely useful; the problem is that the type is named like a std::vector specialisation and silently fails to satisfy the std::vector interface, so generic code that takes a vector<T>& does the wrong thing when T = bool. The fix would be a rename, which the committee will not do, so the trap remains in the standard. Every working engineer learns to write std::deque<bool> or to use a different container. std::shared_ptr's default atomic reference count, the std::initializer_list interaction with auto, the std::function implementation-defined small-buffer optimisation that produces different runtime performance on libstdc++, libc++, and MSVC STL, the std::random_device that the standard explicitly permits to be deterministic. Each of these has named expert commentary calling the design a defect, and none of them are formally deprecated. They are the standard library you ship to production and route around with your own helpers, your own conventions, and your own code-review rules. The volatile saga deserves its own line. volatile was deprecated for compound operations and parameters/returns in C++20 by P1152R4, partially un-deprecated in C++23 by P2327R1 after embedded-community pushback, with further deprecation removals scheduled by P2866R0 for C++26. The committee deprecated a feature, the affected community objected, the committee walked the deprecation back, and now there is a paper to walk part of the walk-back back. This is what fifteen years of standards work on a five-letter keyword looks like. Tier 3: The containers everyone uses and nobody can fix The most damning tier is the standard library containers that the C++ committee cannot deprecate because they are the defaults that every textbook teaches and that ABI compatibility freezes in place. These are the containers a C++ beginner reaches for on the first day of writing real code. Three of them are demonstrably wrong by the standards everyone else has agreed on.