C++库API以std::weak_ptr为输入参数是否合理?存在何种适用场景?
Great question—this is a common point of confusion when working with smart pointers, and your intuition about letting users handle .lock() upfront makes perfect sense for many cases. But there are specific scenarios where accepting std::weak_ptr directly is not just preferable, but necessary. Let’s break them down:
1. Deferred or Asynchronous Operations
If the API schedules work to run later (e.g., in a background thread, a task queue, or a timer callback), requiring a std::shared_ptr would force the object to stay alive for as long as the task is pending—even if the user no longer needs it.
For example, imagine a UI framework that lets you queue an asynchronous update for a widget:
// Bad: API takes shared_ptr, keeps widget alive even after user closes it void queue_widget_update(std::shared_ptr<Widget> widget); // Good: API takes weak_ptr, checks if widget is still alive when executing void queue_widget_update(std::weak_ptr<Widget> widget);
When the task runs later, the framework can call .lock() on the weak_ptr: if it succeeds, the widget is still around to update; if not, the task can safely skip execution. This lets the user control the widget’s lifecycle without being blocked by pending framework tasks.
2. Preventing Accidental Circular References
Suppose the user’s object holds a std::shared_ptr to the library instance, and the library needs to call methods on the user’s object. If the API took a std::shared_ptr to the user’s object, you’d create a circular reference:
- UserObject → LibraryInstance (shared_ptr)
- LibraryInstance → UserObject (shared_ptr)
Circular references break reference counting—neither object will ever be destroyed, leading to memory leaks. By accepting a std::weak_ptr, the library doesn’t add to the reference count, eliminating this risk entirely. This is especially common in event-driven systems, where objects subscribe to library events and the library needs to trigger callbacks.
3. Explicitly Enforcing "No Ownership" Semantics
Using std::weak_ptr as a parameter is a clear, compiler-enforced way to communicate: "This API will not take ownership of your object, nor will it extend its lifecycle."
While a library could technically accept a std::shared_ptr and just not store it, that’s a convention that relies on documentation. A weak_ptr makes the intent unambiguous: users immediately know the object might be gone when the API executes, and they don’t have to worry about accidental reference count increases keeping objects alive longer than intended. For example, a logging API that accepts a weak_ptr<Logger> tells users the logger is only used temporarily, not retained.
4. Batch Operations on Potentially Stale Objects
If the API needs to process multiple objects where some might already be destroyed, having users pre-lock each weak_ptr and pass shared_ptrs adds unnecessary work.
For instance, a resource cleanup API that scans for active resources:
// User has to filter out stale objects first void cleanup_resources(const std::vector<std::shared_ptr<Resource>>& resources); // Library handles the check internally void cleanup_resources(const std::vector<std::weak_ptr<Resource>>& resources);
In the second case, the library can iterate through the weak_ptrs, lock each one, and only process the still-alive resources. This shifts the boilerplate from the user to the library, where it’s better suited.
To circle back to your point: requiring users to pass std::shared_ptr (after calling .lock()) is absolutely reasonable for synchronous, immediate-execution APIs where the user guarantees the object is alive. But the scenarios above are cases where std::weak_ptr is the only way to solve the problem correctly and cleanly.
内容的提问来源于stack exchange,提问作者TonySalimi

