Interconverting std::function with copyable_function

Lobsters Hottest Tools

Summary

Technical analysis of the interconversion between std::function and std::copyable_function in C++26, discussing implementation approaches and performance trade-offs, with specific note that libstdc++ currently uses a less optimal approach.

<p><a href="https://lobste.rs/s/lhnggy/interconverting_std_function_with">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 07/27/26, 07:44 AM

# Interconverting std::function with copyable_function Source: [https://quuxplusone.github.io/blog/2026/07/26/function-explosion/](https://quuxplusone.github.io/blog/2026/07/26/function-explosion/) C\+\+11 introduced`std::function`as a[type\-erased](https://quuxplusone.github.io/blog/2019/03/18/what-is-type-erasure/)callable\-object holder\.`function`was badly designed in a few ways: Its rarely used[“go fish” API](https://quuxplusone.github.io/blog/2019/03/27/design-space-for-std-function/#type-un-erasure)bloats every user; it advertises`operator\(\) const`even when the controlled object’s`operator\(\)`is mutating; it handles what should be a precondition violation by throwing an exception, which again bloats every user and means its`operator\(\)`can never be noexcept\. So C\+\+26 fixed these problems by adding[`std::copyable\_function`](https://en.cppreference.com/cpp/utility/functional/copyable_function)\. \(Which preserves*some*of`function`’s suboptimal design decisions:`copyable\_function<bool\(\)\>`remains implicitly convertible to`copyable\_function<void\(\)\>`, contextually convertible to`bool`, and assignable from`nullptr`\.\) [P2548 “`copyable\_function`”](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf)\(Hava 2023\) contains this guidance for implementors: > It is recommended that implementors do not perform additional allocations when converting from a`copyable\_function`instantiation to a compatible`move\_only\_function`instantiation, but this is left as quality\-of\-implementation\. Note that converting in the other direction — from`move\_only\_function`to`copyable\_function`— is disallowed:`copyable\_function`can hold only copyable callables, and`move\_only\_function`is not copyable\. But`copyable\_function`is interconvertible — both directions — with`std::function`\! Both of these type\-erased wrappers are constructible from anything that is copyable and callable, and themselves*are*copyable and callable\. So we can do this: ``` std::function<int()> f = []{ return 5; }; std::copyable_function<int() const> cf = std::move(f); std::function<int()> g = std::move(cf); ``` You’d hope the move from`f`into`cf`would be implemented something like this: ![](https://quuxplusone.github.io/blog/images/2026-07-26-hopedfor.png) and vice versa for the move back into`g`: ![](https://quuxplusone.github.io/blog/images/2026-07-26-hopedfor2.png) But if`copyable\_function`doesn’t know about`function`, it might just treat`f`the same as any old rvalue of copyable, callable type: it might move a copy of`f`onto the heap to own\. ![](https://quuxplusone.github.io/blog/images/2026-07-26-expected.png) And vice versa for the move back into`g`:`function`might just treat`cf`the same as any old rvalue of copyable, callable type, and move a copy of`cf`onto the heap to own\. ![](https://quuxplusone.github.io/blog/images/2026-07-26-expected2.png) This move operation is still relatively*performant*— it does only O\(1\) work for each construction — but after a sequence of \\\(n\\\) such constructions, we’ve produced a sort of “linked list” of callables of length \\\(n\\\)\. Each time we invoke`g`,`g`’s`operator\(\)`will call the`operator\(\)`of its controlled`copyable\_function`, which calls the`operator\(\)`of its controlled`function`, which calls the`operator\(\)`of its controlled lambda \(the green object in these diagrams\)\. --- As of this writing \(July 2026\), libstdc\+\+ is the only one of the Big Three STL vendors to have implemented C\+\+26`copyable\_function`; and as of this writing, they’ve implemented the “bad” behavior diagrammed above\. Consider the following contrived scenario: We have some kind of “handler callback” stored in a`std::function`\. Periodically we consider whether to modify that handler based on user input, e\.g\. ``` using Handler = std::function<bool(char)>; Handler update(Handler h, bool negate) { if (negate) { h = [f = std::move(h)](char c) { return !f(c); }; } return h; } ~~~~ Handler h = original_handler; while (~~~~) { ~~~~ h = update(std::move(h), (input == '!')); } ``` Observe that`h`is moved into`update`, and moved out again \(thanks to[implicit move](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2266r3.html#wording)\)\. This uses the move constructor of`std::function`, which, like most move constructors, is quick and non\-allocating\. But suppose one of our coworkers updates part of this code to C\+\+26, where we might reasonably prefer to use`std::copyable\_function`in place of`std::function`\.[\[1\]](https://quuxplusone.github.io/blog/2026/07/26/function-explosion/#footnote-should-you-abandon-func)If our coworker updates the whole codebase to use`copyable\_function`consistently, there’s no problem\. But what if they update only their part, so that their new`copyable\_function`\-based code must interoperate with everyone else’s old`function`\-based code? Then we get something like this: ``` using OldHandler = std::function<bool(char)>; using NewHandler = std::copyable_function<bool(char)>; OldHandler update(OldHandler h, bool negate); // unchanged, for the benefit of pre-C++26 callers ~~~~ NewHandler h = original_handler; while (~~~~) { ~~~~ h = update(std::move(h), (input == '!')); } ``` This triggers that “linked list” scenario, and`h`’s call operator gets slower with every round\-trip through`update`— even when`negate`is`false`\! I wrote up this little benchmark \([Godbolt](https://godbolt.org/z/zG9WW6bdK)\): ``` #include <chrono> #include <cstdio> #include <functional> size_t now() { static const auto epoch = std::chrono::high_resolution_clock::now(); auto tp = std::chrono::high_resolution_clock::now(); return std::chrono::duration_cast<std::chrono::microseconds>(tp - epoch).count(); } void print_elapsed_time(int i, size_t call_started) { printf("Invocation on the %dth iteration took %zu us\n", i, now() - call_started); } using OldHandler = std::function<void(int, size_t)>; using NewHandler = std::copyable_function<void(int, size_t) const>; OldHandler update(OldHandler h, bool negate) { if (negate) { /* do something here, but for this example we don't care what */ } return h; } int main() { NewHandler handler = print_elapsed_time; for (int i = 0; i < 160'000; ++i) { handler = update(std::move(handler), false); if (i % 10'000 == 0) { handler(i, now()); } } } ``` Run this program on libstdc\+\+ \(as of GCC 16\) and you’ll see output something like the following: ``` Invocation on the 0th iteration took 0 us Invocation on the 10000th iteration took 180 us Invocation on the 20000th iteration took 296 us Invocation on the 30000th iteration took 365 us [...] Invocation on the 130000th iteration took 1057 us Invocation on the 140000th iteration took 1108 us Invocation on the 150000th iteration took 1202 us ``` Each time we invoke`handler\(i, now\(\)\)`the “linked list of callables” hanging off`handler`is 10,000 elements longer than before, and takes about 100 microseconds longer to chase those pointers\. Of course it also holds onto many kilobytes more memory than needed, too\. This is a “logical memory leak”: that memory is technically still accessible and will be freed by`h`’s destructor, but as long as`h`lives we’ll see our RAM usage creep higher and higher over time, without any change in the “logical” state of our program\. --- As of July 2026, Microsoft STL makes`move\_only\_function`and`function`interoperate without such “logical memory leaks,” and I bet their`copyable\_function`will also avoid them, once they implement that type\. libstdc\+\+’s implementation may improve in the future\. Meanwhile, libc\+\+ hasn’t implemented either`move\_only\_function`or`copyable\_function`yet, so they still have plenty of time to think about this issue\. --- Beware of a simpler version of this pitfall, by the way, when writing your own type\-erased types\. Make sure that when you copy a`Printable`\-holding\-an\-`int`, you get another`Printable`\-holding\-an\-`int`and not a`Printable`\-holding\-a\-`Printable`\-holding\-an\-`int`\. Write unit tests to make sure that still holds when constructing from a`Printable&`; from a`const Printable&`; from a`Printable&&`; and from a`const Printable&&`\. Solving this problem gets more painful if, like the STL, you have more than one type\-erased type that need to play well together\. Try to avoid getting into that situation if you can; but be aware of this pitfall if you must\. --- Footnote:*Should*you abandon`function`for`copyable\_function`in C\+\+26? Some would say yes\. Personally I would say that you ought to abandon both, and write your own type\-erased callable instead\. It takes less than 100 lines\! In completely green\-field C\+\+26 code, sure, I’d recommend`copyable\_function`over`function`\(once your vendor implements it\)\. I would*not*recommend mixing both types in the same brown\-field codebase; I’d pick one and stick with it, while working toward a point where you could update the entire codebase at once\.

Similar Articles

C++26: More function wrappers

Lobsters Hottest

C++26 introduces two new function wrappers: std::copyable_function, which provides a copyable, const-correct alternative to std::function, and std::function_ref, a non-owning callable reference with reference semantics.

Accelerating std::copy_if using SIMD

Lobsters Hottest

Blog post analyzing and implementing a SIMD-accelerated version of std::copy_if using AVX-512 instructions on AMD Zen 4, with performance analysis and comparisons to compiler auto-vectorization.

When can the C++ compiler devirtualize a call?

Hacker News Top

Explores when C++ compilers can devirtualize virtual function calls, covering cases like known dynamic types and final keyword, with comparisons across GCC, Clang, MSVC, and ICC.

const_cast: A Necessary Evil

Lobsters Hottest

The article explains why const_cast is sometimes necessary in C++, specifically for moving objects out of a std::priority_queue, and how to do it safely.

The Fil-C Optimized Calling Convention

Hacker News Top

The Fil-C optimized calling convention ensures memory safety for C programs even under adversarial misuse, while maintaining efficiency by omitting safety checks in the common case. It explains the generic and register-passing optimizations that handle type violations via panics or well-defined behavior.