【问题标题】:why must a Boost.Asio handler be copy constructible?为什么 Boost.Asio 处理程序必须是可复制构造的?
【发布时间】:2016-06-08 18:07:20
【问题描述】:

根据http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/Handler.html,提供给io_service::post 的处理程序必须是可复制构造的。

但是,这不包括接受套接字并移动响应处理程序的情况,保证只有一个处理程序用于该作业:

auto socket = std::make_unique<socket>();
accepter.accept(*socket);

service.post([s{std::move(socket)}] {
  asio::write(*s, buffer("response"), ignored_err);
});

那么为什么这个副本是可构造的要求呢?

-- 编辑--

澄清:我想让我的代码确保只存在一个处理程序实例。这使得推理变得容易得多。

对于很多程序来说,CopyConstructible 要求太严格,MoveConstructible 更合适。

    auto i = std::make_unique<int>();
    auto handler_w_resource = [i{std::move(i)}]{ ++(*i);};
    // auto copied = handler_w_resource; --> +1: lambda can't be copied!
    auto moved = std::move(handler_w_resource);

因此我很惊讶它无法搬进来:

    service.post(std::move(moved)); // :( post accepts no rvalue

【问题讨论】:

  • 唯一指针有效吗? Tbh 我还没有测试我的 1.60 版本,但至少在 1.56 之前,套接字必须在共享指针中......
  • 独立ASIO是否需要?一段时间以来,我一直在 C++11 标准中使用独立的 ASIO,并且一些仅限 C++11 的特权,例如 move-semantics 和 std::function,就像我期望的那样工作。可能需要复制可构造性,因为 boost::asio 必须支持 C++03,并且说“如果 C++03 可复制构造,但仅在 C++11 时才需要移动构造”可能太复杂了。只是猜测,我不确定。
  • 似乎他们通过 boost v1.67 reference 降低了对 Handler 的 MoveConstructible 的要求

标签: c++ boost boost-asio move-semantics


【解决方案1】:

我无法找到任何材料来解释为什么处理程序必须是CopyConstructible,但即使存在 C++11 支持,也明确指出了这一点。 Movable Handlers 文档说明:

作为优化,用户定义的完成处理程序可以提供移动构造函数,而 Boost.Asio 的实现将优先使用处理程序的移动构造函数而不是其复制构造函数。在某些情况下,Boost.Asio 可能能够消除对处理程序的复制构造函数的所有调用。但是,处理程序类型仍然需要是可复制构造的。

当不满足类型要求时,Asio 执行的类型检查允许更友好的编译器错误消息。这种类型检查在调用堆栈中较早发生,并且不以使用该对象是否会产生编译器错误为前提。例如,当启用类型检查时,类型检查将为没有复制构造函数的处理程序发出编译器错误,即使对处理程序的复制构造函数的所有调用都已被消除。通过定义BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS,可以禁用显式类型检查,并允许编译器错误从实现中更深层次的调用点出现(如果发生)。 History 注明了这个选项:

Asio 1.6.0 / Boost 1.47

  • ...
  • 在完成处理程序不满足必要的类型要求时添加了更友好的编译器错误。当 C++0x 可用时(目前支持 g++ 4.5 或更高版本,以及 MSVC 10),static_assert 也用于生成信息性错误消息。可以通过定义 BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS 来禁用此检查。

这是一个完整的例子demonstrating这个功能。在其中,unique_ptr 管理的套接字的所有权通过std::move() 转移给处理程序:

#include <functional> // std::bind
#include <memory>     // std::unique_ptr
#include <string>     // std::string
#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
#include <boost/asio.hpp>

const auto noop = std::bind([]{});

int main()
{
  using boost::asio::ip::tcp;  

  // Create all I/O objects.
  boost::asio::io_service io_service;
  tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
  auto socket1 = std::make_unique<tcp::socket>(std::ref(io_service));
  tcp::socket socket2(io_service);

  // Connect the sockets.
  acceptor.async_accept(*socket1, noop);
  socket2.async_connect(acceptor.local_endpoint(), noop);
  io_service.run();
  io_service.reset();

  // Move ownership of socket1 to a handler that will write to the
  // socket.
  const std::string expected_message = "test message";
  io_service.post([socket1{std::move(socket1)}, &expected_message] {
    boost::asio::write(*socket1, boost::asio::buffer(expected_message));
  });
  io_service.run();

  // Read from socket2.
  std::vector<char> actual_message(socket2.available());
  boost::asio::read(socket2, boost::asio::buffer(actual_message));

  // Verify message.
  assert(std::equal(
    begin(expected_message), end(expected_message),
    begin(actual_message), end(actual_message)));
}

【讨论】:

  • 不管怎样,在当前的networking-ts draft中,CompletionHandler类型必须满足DestructibleMoveConstructible的要求。
【解决方案2】:

这只是一个约定。

这种约定可以在许多 C++ 库中找到,包括所有标准库算法

约定让我们更容易

  • 正确实现语义
  • 知道会发生什么
  • 让编译器进行深度优化(由于别名问题,引用扼杀了许多优化)

所以,如果您需要一个引用处理程序,只需传递std::ref(f)。它是为此目的而设计的。


提防生命周期问题 - 在使用引用时一如既往。

【讨论】:

  • 你总结了三个论点反对复制构造。正是我问这个问题的原因。在这种情况下,我的处理程序会被复制实际上是非常意外。这意味着我可以为一个套接字设置两个处理程序。如果可以将处理程序移入,则更容易正确实现语义,并且编译器将对周围处理程序的数量有更多的保证,因此可以进行更深入的优化。
  • @xtofl 我列出了客观原因。您是说标准库算法旨在损害性能吗?您是否真的比较了生成的程序集以了解编译器如何优化各种方法?对于您的评论,我能想到的唯一合乎逻辑的解释是“但是!我有一个昂贵的复制课程”或“但我想避免复制,因为我出于其他目的计算实例”。在这种情况下,您使用该 reference_wrapper。问题解决了。关键是:按价值取函数给选择。
  • 对不起,我必须错过一些东西,但我的问题是我无法选择,因为 postpostpost 操作不支持移动语义。所以我更期待一个答案,比如“因为在设计这个库时还不知道 Move Semantics”。换句话说:这个约定是在引入移动语义之前还是之后选择的?
  • (避免“大型完成处理程序”的 Asio 惯用方法是在处理程序关闭的对象实例上使用 enable_shared_from_this。您可以在这里看到并排的 stackoverflow.com/a/32725856/85371。当然你不需要使用 shared_ptr 的引用计数来管理生命周期。如果你用其他方式,std::reference_wrapper&lt;&gt; 就可以了)
  • @sehe:我认为您缺少的是 xtofl 想要 值语义,而不是引用语义;他只是想(有效地)移动价值而不是复制它。您的回答和 cmets 归结为“放弃价值语义并改用引用语义”,这不是所要求的。
猜你喜欢
  • 1970-01-01
  • 2019-10-06
  • 2019-08-05
  • 2020-10-07
  • 2011-07-06
  • 2015-05-29
  • 1970-01-01
  • 2013-06-02
相关资源
最近更新 更多