在单个语句中移动和使用唯一指针是否有效?

Is it valid to move and use unique pointer in single statement?

本文关键字:指针 唯一 是否 有效 语句 单个 移动      更新时间:2024-09-21

我在代码的下面使用堆栈跟踪时遇到分段错误,如下所述。self在这里是一个unique_ptr

self->socket.async_send_to(self->frame->get_asio_buffer(), self->client_endpoint,
std::bind(&download_server::receiver, std::move(self), std::placeholders::_1, 
std::placeholders::_2));

#3 是上面提到的代码行。

==1652==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000090 (pc 0x55e763e1bdc9 bp 0x7ffc2242d380 sp 0x7ffc2242d370 T0)
==1652==The signal is caused by a READ memory access.
==1652==Hint: address points to the zero page.
#0 0x55e763e1bdc9 in std::__shared_ptr<tftp::frame, (__gnu_cxx::_Lock_policy)2>::get() const /usr/include/c++/10.2.0/bits/shared_ptr_base.h:1325
#1 0x55e763e1a9d5 in std::__shared_ptr_access<tftp::frame, (__gnu_cxx::_Lock_policy)2, false, false>::_M_get() const /usr/include/c++/10.2.0/bits/shared_ptr_base.h:1024
#2 0x55e763e1948d in std::__shared_ptr_access<tftp::frame, (__gnu_cxx::_Lock_policy)2, false, false>::operator->() const /usr/include/c++/10.2.0/bits/shared_ptr_base.h:1018
#3 0x55e763e0d665 in tftp::download_server::sender(std::unique_ptr<tftp::download_server, std::default_delete<tftp::download_server> >&, boost::system::error_code const&, unsigned long) ../src/tftp_server.cpp:50
#4 0x55e763e0cf56 in tftp::download_server::serve(boost::asio::io_context&, std::shared_ptr<tftp::frame const> const&, boost::asio::ip::basic_endpoint<boost::asio::ip::udp> const&) ../src/tftp_server.cpp:27
#5 0x55e763e0da65 in spin_tftp_server(boost::asio::io_context&, std::shared_ptr<tftp::frame const> const&, boost::asio::ip::basic_endpoint<boost::asio::ip::udp> const&) ../src/tftp_server.cpp:97
#6 0x55e763e0e3f5 in tftp::distributor::perform_distribution_cb(boost::system::error_code const&, unsigned long const&) ../src/tftp_server.cpp:142
#7 0x55e763e273fe in void std::__invoke_impl<void, void (tftp::distributor::*&)(boost::system::error_code const&, unsigned long const&), std::shared_ptr<tftp::distributor>&, boost::system::error_code const&, unsigned long const&>(std::__invoke_memfun_deref, void (tftp::distributor::*&)(boost::system::error_code const&, unsigned long const&), std::shared_ptr<tftp::distributor>&, boost::system::error_code const&, unsigned long const&) /usr/include/c++/10.2.0/bits/invoke.h:73

在上面的代码中,self在单个语句中使用和移动。它有效吗?如果是,那么self的使用顺序是什么?

跟踪文件(文件/usr/include/c++/10.2.0/bits/shared_ptr_base.h)

1322       /// Return the stored pointer.
1323       element_type*
1324       get() const noexcept
1325       { return _M_ptr; }
1326

根据第 1322 行的评论,上述用法似乎无效。在运行时,它无法找到唯一指针指向的数据。这个读数正确吗?

但是,如果上述假设是正确的,那么下面的示例代码也应该崩溃。在这里,self->t.async_wait做了类似的事情(在同一语句中移动和使用),但是这没有任何麻烦。

#include <iostream>
#include <functional>
#include <boost/asio.hpp>
class looper {
public:
static void create(boost::asio::io_context &io){
std::unique_ptr<looper> self = std::make_unique<looper>(io);
start(self, boost::system::error_code());
}
static void start(std::unique_ptr<looper> &self, const boost::system::error_code e){
std::cout << "Round :" << self->count << std::endl;
if(self->count-- == 0){
return;
}
self->t.expires_after(boost::asio::chrono::seconds(1));
self->t.async_wait(std::bind(&looper::start, std::move(self), std::placeholders::_1));
}
looper(boost::asio::io_context &io) : t(io) {
std::cout << "Construction" << std::endl;
}
~looper() {
std::cout << "Destruction" << std::endl;
}
boost::asio::steady_timer t;
uint32_t count = 3;
};
void f(boost::asio::io_context &io){
looper::create(io);
}
int main() {
boost::asio::io_context io;
f(io);
io.run();
return 0;
}

示例程序输出

[root@archlinux cpp]# g++ unique_async.cpp  -lpthread -fsanitize=address -Wpedantic
[root@archlinux cpp]# ./a.out
Construction
Round :3
Round :2
Round :1
Round :0
Destruction
[root@archlinux cpp]#

如果std::move是问题,那么为什么第一种情况崩溃但示例程序运行没有任何问题。

move(self)本身没有问题。它是对bind的嵌套调用和访问self的其他参数的组合。对bind的调用在其他参数访问之前清空self

您有两个展示案例:

// bad
self->socket.async_send_to(self->frame->get_asio_buffer(),
self->client_endpoint,
bind(&download_server::receiver, move(self), _1, _2));
// good
self->t.async_wait(bind(&looper::start, move(self), _1));

在 C++17 之前,这两种情况都有未定义的行为,因为参数是以未指定的顺序计算的,并且也是未排序的。这包括函数调用之前((之前)的对象表达式。

从 C++17 开始,参数表达式的顺序不确定,但对象表达式在其他参数之前排序。其后果是:

  • 在不好的情况下,可以在其他参数之前调用bind,这将清除self,导致在计算其他参数时访问空指针。
  • 然而,在好的情况下,当self仍然有效时,首先调用对象表达式,然后调用bind表达式,这将清除self;但由于没有其他参数要计算,因此调用是可以的。