关于std::function移动赋值非noexcept但swap为noexcept的实现与原因问询
Great question—this gets into some of the nuanced design choices behind std::function! Let's break this down into two parts: how the noexcept swap works, and why the move assignment operator can't be noexcept.
std::function's swap achieves noexcept std::function is a type-erasing wrapper, which typically uses one of two strategies under the hood:
- Small Object Optimization (SSO): For small callable objects (like function pointers, tiny lambdas, or lightweight functors), it stores the object directly in an internal buffer within the
std::functioninstance. - Dynamic Allocation: For larger callables, it allocates memory on the heap and stores a pointer to that memory.
The swap operation for std::function only needs to exchange the internal state of the two instances. Here's why that can be done without exceptions:
- If both instances use SSO, swapping just involves copying bytes between their internal buffers—this is a raw memory operation, no dynamic allocation or destructor calls are involved, so it's inherently
noexcept. - If one or both use dynamic allocation, swapping just exchanges the pointers to the heap-allocated storage. Again, this is a simple pointer swap, no memory is allocated or freed, so no exceptions can be thrown.
Even if the callable objects themselves have non-noexcept operations, swap doesn't invoke them—it just swaps the handles to those objects. That's why the standard guarantees void swap(function&) noexcept;.
You can think of a simplified version of this logic like:
template<typename Signature> class function { private: // Simplified internal state: either a buffer for SSO or a pointer union { std::byte sso_buffer[sizeof(void(*)(void))]; void* heap_ptr; } state; // Type info to manage the stored callable const void* vtable; public: void swap(function& other) noexcept { // Swap the union contents and vtable pointer—all noexcept operations std::swap(state, other.state); std::swap(vtable, other.vtable); } };
std::function's move assignment isn't noexcept The move assignment operator (function& operator=(function&&);) has a different job than swap: it needs to:
- Transfer ownership of the callable from the source
std::functionto the target. - Destroy the callable that the target was previously holding (if any).
The problem lies in step 2: if the target's old callable has a destructor that throws an exception (yes, the standard allows this, even though it's terrible practice), then destroying it during move assignment will cause the entire operator to throw. Since the standard can't guarantee that this destruction won't throw, the move assignment operator can't be marked noexcept.
Compare this to swap: swap never destroys any callables—it just swaps which std::function owns them. No destructors are called, so no risk of exceptions.
This is why you can work around it with default construction + swap:
std::function<void()> target; std::function<void()> source = [](){ /* ... */ }; // This is noexcept, because default ctor is noexcept and swap is noexcept target.swap(source);
Here, the default-constructed target has no callable to destroy, so swapping just transfers the source's state without any destruction steps. But the move assignment operator has to handle the case where the target already holds a callable, which introduces the exception risk.
内容的提问来源于stack exchange,提问作者milleniumbug

