唯一的 ptr 将所有权移动到包含对象的方法

Ada*_*dam 5 c++ shared-ptr boost-asio unique-ptr

我想将 unique_ptr 移至其对象的方法:

class Foo {
    void method(std::unique_ptr<Foo>&& self) {
        // this method now owns self
    }
}

auto foo_p = std::make_unique<Foo>();
foo_p->method(std::move(foo_p));

Run Code Online (Sandbox Code Playgroud)

这可以编译,但我不知道这是否不是未定义的行为。因为我在调用对象的方法时也离开了该对象。

是UB吗?

如果是的话,我可能可以用以下方法修复它:

auto raw_foo_p = foo_p.get();
raw_foo_p->method(std::move(foo_p))
Run Code Online (Sandbox Code Playgroud)

正确的?


(可选动机:)

传递对象以延长其生命周期。它会存在于 lambda 中,直到异步调用 lambda。(boost::asio) 请先看,Server::accept然后看Session::start

您可以看到原始实现使用了shared_ptr,但我不明白为什么这是合理的,因为我只需要我的Session对象的一个​​所有者。

Shared_ptr使代码变得更加复杂,当我不熟悉shared_ptr时很难理解。

#include <iostream>
#include <memory>
#include <utility>

#include <boost/asio.hpp>

using namespace boost::system;
using namespace boost::asio;
using boost::asio::ip::tcp;


class Session /*: public std::enable_shared_from_this<Session>*/ {
public:
    Session(tcp::socket socket);

    void start(std::unique_ptr<Session>&& self);
private:
    tcp::socket socket_;
    std::string data_;
};

Session::Session(tcp::socket socket) : socket_(std::move(socket))
{}

void Session::start(std::unique_ptr<Session>&& self)
{
    // original code, replaced with unique_ptr
    // auto self = shared_from_this();

    socket_.async_read_some(buffer(data_), [this/*, self*/, self(std::move(self))]  (error_code errorCode, size_t) mutable {
        if (!errorCode) {
            std::cout << "received: " << data_ << std::endl;
            start(std::move(self));
        }

        // if error code, this object gets automatically deleted as `self` enters end of the block
    });
}


class Server {
public:
    Server(io_context& context);
private:
    tcp::acceptor acceptor_;

    void accept();
};


Server::Server(io_context& context) : acceptor_(context, tcp::endpoint(tcp::v4(), 8888))
{
    accept();
}

void Server::accept()
{
    acceptor_.async_accept([this](error_code errorCode, tcp::socket socket) {
        if (!errorCode) {
            // original code, replaced with unique_ptr
            // std::make_shared<Session>(std::move(socket))->start();

            auto session_ptr = std::make_unique<Session>(std::move(socket));
            session_ptr->start(std::move(session_ptr));
        }
        accept();
    });
}

int main()
{
    boost::asio::io_context context;
    Server server(context);
    context.run();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

编译为: g++ main.cpp -std=c++17 -lpthread -lboost_system

wal*_*nut 4

对于您的第一个代码块:

\n\n

std::unique_ptr<Foo>&& self是一个引用并为其分配一个参数std::move(foo_p),其中foo_p是一个命名std::unique_ptr<Foo>只会将引用绑定selffoo_p,这意味着将在调用范围中self引用。foo_p

\n\n

它不会创建任何新的std::unique_ptr<Foo>被管理对象的所有权Foo可以转移的对象。不会发生移动构造或赋值,并且对象仍然会随着调用范围内Foo的销毁而被销毁。foo_p

\n\n

因此,此函数调用本身不存在未定义行为的风险,尽管您可能以可能self导致主体中出现未定义行为的方式使用引用。

\n\n

也许您打算成为selfastd::unique_ptr<Foo>而不是std::unique_ptr<Foo>&&。在这种情况下,self它不是一个引用,而是一个实际对象,Foo如果调用 with ,则托管的所有权将通过移动构造转移到该实际对象,并且在函数与托管一起std::move(p_foo)调用后将被销毁。foo_p->method(std::move(foo_p))Foo

\n\n

此替代变体本身是否具有潜在的未定义行为取决于所使用的 C++ 标准版本。

\n\n

在 C++17 之前,允许编译器选择在评估 之前评估调用的参数(以及参数的关联移动构造)foo_p->method。这意味着,它foo_p可能已经从foo_p->method评估时移出,导致未定义的行为。可以按照与您建议的方式类似的方式来修复此问题。

\n\n

从 C++17 开始,可以保证后缀表达式(此处foo_p->method)在调用的任何参数之前计算,因此调用本身不会出现问题。(身体仍然可能导致其他问题。)

\n\n

后一种情况的详细信息:

\n\n

foo_p->method被解释为(foo_p->operator->())->method,因为std::unique_ptr提供了这个operator->()(foo_p->operator->())将解析为指向Foo由 管理的对象的指针std::unique_ptr。最后一个->method解析为该对象的成员函数method。在 C++17 中,此评估发生在对任何参数进行评估之前method,因此是有效的,因为尚未foo_p发生任何移动。

\n\n

那么参数的评估顺序在设计上是未指定的。所以可能A) unique_ptr可以从之前的参数被初始化时foo_p移走。thisB )它将在运行时移出并使用初始化.methodthis

\n\n

A)不是问题,因为 \xc2\xa7 8.2.2:4,正如预期的那样:

\n\n
\n

如果函数是非静态成员函数,则函数的this参数应使用指向调用对象的指针进行初始化,

\n
\n\n

(我们知道这个对象在任何参数被求值之前就已经被解析了。)

\n\n

B)并不重要,因为:(另一个问题

\n\n
\n

C++11 规范保证将对象的所有权从一个 unique_ptr 转移到另一个 unique_ptr 不会更改对象本身的位置

\n
\n\n
\n\n

对于你的第二个块:

\n\n

self(std::move(self))创建一个std::unique_ptr<Session>用引用初始化的类型(不是引用)的 lambda 捕获self,该引用session_ptr在 lambda in 中引用accept。通过移动构造,对象的所有权从lambda 的成员Session转移。session_ptr

\n\n

然后将 lambda 传递给async_read_some,这将(因为 lambda 不是作为非常量左值引用传递)将 lambda 移动到内部存储中,以便稍后可以异步调用它。通过此移动,对象的所有权Session也转移到 boost::asio 内部。

\n\n

async_read_some立即返回,因此 的所有局部变量start和 lambda 都accept被销毁。然而,所有权Session已经转移,因此这里不存在由于生命周期问题而导致的未定义行为。

\n\n

将异步调用 lambda 的副本,这可能会再次调用start,在这种情况下, 的所有权Session将转移到另一个 lambda 的成员,并且具有所有权的 lambdaSession将再次移动到内部 boost::asio 存储。异步调用 lambda 后,它将被 boost::asio 销毁。然而此时,所有权已经再次转移。

\n\n

Session对象最终被销毁,当if(!errorCode)并且拥有该对象的 lambdastd::unique_ptr<Session>在调用后被 boost::asio 销毁。

\n\n

因此,对于与Session\ 的生命周期相关的未定义行为,我认为这种方法没有问题。如果您使用的是 C++17 那么也可以&&std::unique_ptr<Session>&& self

\n