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

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

我在下面的代码行中使用堆栈跟踪遇到分段错误,如下所述。 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;但由于没有其他参数需要评估,调用没问题。