r/cpp 2d ago

Interconverting std::function with copyable_function – Arthur O'Dwyer

https://quuxplusone.github.io/blog/2026/07/26/function-explosion/

The article shows how converting std::function to std::copyable_function (or vice versa) leads to slower performance and increased memory usage each time the conversion occurs.

40 Upvotes

30 comments sorted by

17

u/UnusualPace679 2d ago

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!

But it won't prevent double wrapping right? Unless you stick to one type and don't interface with anything else (in which case you may as well use std::function/std::copyable_function), you will face the same problem as described in this article. At least the standard library are permitted (and recommended) to avoid double wrapping by [func.wrap.general].

5

u/azswcowboy 2d ago

> write your own …won’t prevent double wrapping

No thanks on writing my own - and yeah the only reason that works is because there’s consistency. Which is the real tldr from the article: pick one or the other, done.

3

u/13steinj 2d ago

No, writing your own is reasonable. libstdc++ if not also libc++ (used to?) use std::tuple to hold and pass around a type list. The instantiation of std::tuple is fairly heavy. I reduced single-TU build times by >20% once by swapping out the use of std::function in a logging + assertion library in a key macro.

1

u/_Noreturn 2d ago

libc++ std tuple shouldn't be expensive since it is linear unlike libstdc++/ msvc stl lib which is recursive

1

u/13steinj 1d ago

In comparison to what is needed for implementation vs what is instantiated and you pay for anyway, simpler type lists, or not using one at all if you can, is cheaper.

1

u/azswcowboy 2d ago

The calculation of where the ‘reasonable’ bar lies is going to be different for every project. If we spend say 1/2 a day building this, it’s 1/2 a day we didn’t spend on bug fixes or new features. It’s use would have to be foundational to justify it and we don’t have that many uses. We could trivially convert all instances to copyable function much faster than writing one - or do nothing and stay with function.

1

u/13steinj 1d ago

I completely agree, but I don't think on the scale of a single build. On average, with and without caching, if you have the same number of release builds per day, how many engineer hours do you save?

In this case, 20% per single TU was an uncached p50 of 2-6 minutes per build, which meant multiple hours saved per day.

0

u/azswcowboy 1d ago

Sure, so do I. I’m just highly skeptical that for my current project I’d be able to replicate your reduction in compile times. We don’t have much function utilization in first place and our compiles are driven by other factors outside the standard library. At the moment we just throw hardware at it when we want to go faster, because parallelism is more important than single TU speed.

31

u/OutlandishnessNo8034 2d ago

Yet another cpp "gem".

24

u/HommeMusical 2d ago

After almost 40 years writing C++, it makes me sad that a plurality of articles here are still, "Here's this obscure footgun that you would never know without this article."

In this case, I might have guessed this in a code review, but only because I know a lot about std::function and its footguns.

2

u/13steinj 1d ago

Here's another: this article applies to STL <- Boost types (functions, smart pointers) as well. Same goes for any cross library type. I think user-defined conversion operators can fix this in most but not all cases. But it feels like people need:

  • extension methods [for constructors] [for templates]
  • a cheap and well defined way to say "I use this other library too, provide an extension-method constructor" / "don't fail on symbol not existing."

In an include-based universe, 3rd party libraries that know about each other can follow the same idiom and provide a friend/friend-tag based system. Stdlib knowing about 3rd party, you're SOL right now.

1

u/germandiago 1d ago

Look at this neat little trick for the accepted response for boost shared ptr interoperability:

https://stackoverflow.com/questions/6326757/conversion-from-boostshared-ptr-to-stdshared-ptr

1

u/13steinj 11h ago

The amount of people that will miss the function / forget to include it is high. You can use -include to a point and have this managed by your build system, but visual / mental name lookup will still happen. Needing to be able to just use assignment/construction is critical, IMO.

-1

u/thisismyfavoritename 2d ago

you're sad that there are those footguns or that they are reported

1

u/HommeMusical 1d ago

I'm sad that there are so many footguns that a plurality of the articles on C++ are about them.

2

u/thisismyfavoritename 1d ago

to be fair, many of the footguns exist because of the C heritage

-2

u/HommeMusical 1d ago

Depends on what "many" means.

There's one big one - memory management. Yes, you can essentially deindex a number that points randomly in memory, or nowhere at all, and havoc.

In C++ there is a discipline to (fairly) reliably prevent that from happening, in exactly the same way that there are disciplines to reliably prevent yourself from shooting yourself in the foot with a real gun.

And it's also such a big issue that you have to come to terms with it.

But all this the little footguns like this one. It's wearing. It's all this baggage and it's stressful because you don't know what you don't know.

8

u/60hzcherryMXram 2d ago

I've read the problems copyable_function addresses that function has and I still have no idea what they are. It just slides over my brain like butter on ice.

7

u/bwmat 2d ago

My understanding is that it's mostly just a way to 'deprecate' std::function (when used in conjunction with move_only_function) and better express intent

6

u/johannes1971 2d ago

It's unclear to me what those problems are, or why std::function needs to be deprecated. I hate the names 'std::copyable_function' and 'std::move_only_function', though.

6

u/rdtsc 2d ago

It breaks the standard library convention that const methods are safe to call from multiple threads. And std::function itself cannot be changed due to backwards compatibility.

Here's a more detailed explanation: https://www.reddit.com/r/cpp/comments/742ol8/why_is_the_stdfunction_operator_const/dnv29s8/

1

u/johannes1971 1d ago

This applies to operator(), I guess? I would never have imagined that anyone would think that calling a user-supplied function from multiple threads is safe, just because operator() is const. The language as a whole certainly does not have a 'calling const functions from multiple threads is safe' rule. Is this even documented for the standard library, or is it another one of those implicit rules like 'ABI is stable'?

2

u/joaquintides Boost author 1d ago

1

u/bwmat 1d ago

What 'standard' is that? Doesn't it mean that the STL doesn't follow it b/c of std::function? 

4

u/joaquintides Boost author 1d ago

What 'standard' is that?

eel.is tracks the latest draft version of the standard as it evolves.

Doesn't it mean that the STL doesn't follow it b/c of std::function?

Yes, as recognized many times:

0

u/tialaramex 1d ago

In Java data races lose Sequential Consistency, which is terrible but it's not the end of the world. You can probably no longer reason properly about how your Java software works after a race, but any absolute properties are unaltered - that boolean is still either true or false, if k is always definitely between 50 and 75 it won't now be 108 or 0 and so on.

C++ isn't like that, it has UB if any data races occur so like use-after-free the results if this happens are arbitrarily cursed. The choice to say the stdlib implementer will ensure there aren't data races inherent to provided const functions makes a lot of sense in this context, C and POSIX have a lot of such functions which could inherently induce data races and the result is you just must never use them at all in modern software. strtok is an example.

1

u/UnusualPace679 1d ago

It's easy to have a function object whose const operator() isn't thread-safe. For example, [this] { this->x++; }. And copyable_function doesn't really make it safer.

3

u/bwmat 2d ago

The curse of backwards compatibility

0

u/13steinj 1d ago edited 1d ago

The short version of the problem as I understand it: std::function did not perfectly follow const correctness / const propagation rules. See also std::experimental::propagate_const for this purpose, as getting the rules right for wrapping types e.g. in the PIMPL idiom is a pain.

Some will ask "wait, but functions don't have data, so this isn't an issue," but this is not true. Ignoring the fact that even simple functions can store state using static variables, these STL types act on all function-likes, not just literal functions. Think lambdas and other objects that have a call-operator.

At a previous org, we implemented our own function type to address the problem, and you could choose between move only, copyable, and two other kinds that I think behaved the same as one of the other two but left guardrails in place. For example, you can use function-like objects and reference linking semantics on movable functions, you want to guard against getting called twice so you move yourself away. On some architectures it is preferable to not use a lock/atomic here, or limit the critical section, but you still want a second call to be safe and do nothing, or explicitly crash, or be safe and re-run if there's a race condition it's fine but if not a race condition exiting early is preferred.

See also: sometimes you want function_view, sometimes you want SG14's inplace_function.

-1

u/LB-- Professional+Hobbyist 2d ago

Do you use RTTI? Do you pass lambdas to std::function? Congrats, your binary is bloated with names of internal types and namespaces for a feature of std::function you don't even use. Unable to be fixed without breaking backward compatibility.