我对我经常看到的 Boost.Asio 习惯用法感到困惑 - 像这样调用处理程序(函数对象):
std::move(handler)(param1, param2);
这样写的原因是什么?我的理解是这和
完全一样handler(param1, param2);
除非处理程序的
operator()
方法使用 &&
进行引用限定(有关引用限定的信息,请参阅“带有引用限定符的成员函数”here)。这是可以预料的吗?我从来没有真正见过这个成语与引用限定operator()
配对,所以这似乎是一个不太可能的解释。
例子:
std::move(op)
test_deferred()
一般来说,对 move-only 处理程序的明确支持与分配顺序相关保证Asio makes:
如前所述,必须在调用完成处理程序之前释放所有资源。
这使得内存能够被回收用于代理内的后续异步操作。这允许具有长寿命异步代理的应用程序没有热路径内存分配,即使用户代码不知道关联的分配器。
具体来说,第二个例子
auto op = async_write_messages(socket, "Testing deferred\r\n", 5, asio::deferred);
定义一个打包操作
op
与类型
asio::deferred_async_operation<
void(boost::system::error_code),
asio::detail::initiate_composed_op<void(boost::system::error_code),
void(asio::any_io_executor)>,
async_write_messages_implementation>
看看它的实现,我们确实看到了你的猜测:
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(Signature) CompletionToken>
auto operator()(
BOOST_ASIO_MOVE_ARG(CompletionToken) token) BOOST_ASIO_RVALUE_REF_QUAL;
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(Signature) CompletionToken>
auto operator()(
BOOST_ASIO_MOVE_ARG(CompletionToken) token) const &;
其中,去除宏噪声,在 c++20 中变为:
template <asio::completion_token_for<Signature> CompletionToken>
auto operator()(CompletionToken&& token) &&;
template <asio::completion_token_for<Signature> CompletionToken>
auto operator()(CompletionToken&& token) const&;
&&
限定的重载优化了执行。当您意识到延迟处理程序可能仅表示 deferred_values
,将被传递给用户处理程序时,这在直觉上是有意义的。这些 - 有效的回调参数 - 复制或仅移动也可能很昂贵。
在这种情况下,
deferred_async_operation
实现了一个延迟启动另一个异步操作的函数对象。启动函数采用的参数可能再次复制起来很昂贵,或者只能移动。
实际上,右值引用限定版本支持那些移动语义,而 const 限定版本不支持(再次为易读性对 Asio 代码进行了大量编辑,并假设 C++14 或更高版本):
template <typename CompletionToken, std::size_t... I>
auto invoke_helper(CompletionToken&& token, std::index_sequence<I...>)
{
return asio::async_initiate<CompletionToken, Signature>(
std::move(initiation_), token, std::get<I>(std::move(init_args_))...);
}
template <typename CompletionToken, std::size_t... I>
auto const_invoke_helper(CompletionToken&& token, std::index_sequence<I...>) const&
{
return asio::async_initiate<CompletionToken, Signature>( //
initiation_t(initiation_), token, std::get<I>(init_args_)...);
}
可以说在不支持引用限定的代码中,
operator()
的非限定版本将成功地将左值引用传递给启动函数。这甚至适用于仅移动类型 IFF 参数由左值引用获取。如果是可变的,那些甚至可以从中移动。
更精确的转发允许启动,其中 move-only(“sink”)参数也被取值。
在避免复制的情况下,这具有优化应用程序(取消)分配模式的重要好处。考虑如果涉及的参数之一包含引用计数资源(例如
shared_ptr
)会发生什么。即使包装类型(例如deferred_async_operation
)在调用后“立即”消失,当引用计数资源暂时具有非唯一引用计数时,分配/取消分配的顺序可能会有所不同。
我想把它归结为 富有表现力的代码:只被调用一次的可调用对象应该表达“唯一调用”,就像任何其他仅移动类型信号“唯一所有权”一样,因为存在
std::move()
.
有些地方很重要,像 Asio 这样的通用库不应该强加不必要的开销。
在示例中,
async_write_messages_implementation
是只能移动的,因为它包含 unique_ptr
成员。因此,通过const_invoke_helper
就无法编译:https://godbolt.org/z/KTc5ooPhT
您可以通过将
unique_ptr
更改为 shared_ptr
来修复它,但是现在您遇到了所描述的问题,即在调用 async_write_messages_implementation::operator()
期间资源的所有权将不是唯一的,这会导致 reset()
s 不释放他们的资源直到以后:https://godbolt.org/z/n9fbE3zxh