Boost.Asio - 在调用之前使用 std::move on handler

问题描述 投票:0回答:1

我对我经常看到的 Boost.Asio 习惯用法感到困惑 - 像这样调用处理程序(函数对象):

std::move(handler)(param1, param2);

这样写的原因是什么?我的理解是这和

完全一样
handler(param1, param2);

除非处理程序的

operator()
方法使用
&&
进行引用限定(有关引用限定的信息,请参阅“带有引用限定符的成员函数”here)。这是可以预料的吗?我从来没有真正见过这个成语与引用限定
operator()
配对,所以这似乎是一个不太可能的解释。

例子:

c++ boost boost-asio move-semantics
1个回答
0
投票

一般来说,对 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
)在调用后“立即”消失,当引用计数资源暂时具有非唯一引用计数时,分配/取消分配的顺序可能会有所不同。

TL;博士

我想把它归结为 富有表现力的代码:只被调用一次的可调用对象应该表达“唯一调用”,就像任何其他仅移动类型信号“唯一所有权”一样,因为存在

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

© www.soinside.com 2019 - 2024. All rights reserved.