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

Unique ptr move ownership to containing object's method

我想将 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));

这个编译通过了,不知道是不是Undefined behavior。因为我在调用它的方法时从对象中移出。

是UB吗?

如果是,我可能会修复它:

auto raw_foo_p = foo_p.get();
raw_foo_p->method(std::move(foo_p))

对吗?


(可选动机:)

传递对象以延长其寿命。它会存在于 lambda 中,直到 lambda 被异步调用。 (提升::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;
}

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

对于您的第一个代码块:

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

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

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

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

一起销毁

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

在 C++17 之前,允许编译器选择在评估 foo_p->method 之前评估调用的参数(以及参数的相关移动构造)。这意味着 foo_p 可能已经从评估 foo_p->method 时移动,导致未定义的行为。这可以像您建议的那样解决。

从 C++17 开始,保证 postfix-expression(这里是 foo_p->method)在调用的任何参数之前被求值,因此调用本身不会成为问题。 (body 仍然可能导致其他问题。)

后一种情况的详细信息:

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

然后参数的评估顺序是设计未指定的。所以可能 A) unique_ptr foo_p 可以从 this 之前移动,因为参数将被初始化。并且 B)method 运行时移动并使用初始化的 this.

但是 A) 不是问题,因为 § 8.2.2:4,正如预期的那样:

If the function is a non-static member function, the this parameter of the function shall be initialized with a pointer to the object of the call,

(我们知道这个 object 在 任何参数被评估之前 已经解决。)

B) 无关紧要,因为:(another question)

the C++11 specification guarantees that transferring ownership of an object from one unique_ptr to another unique_ptr does not change the location of the object itself


你的第二个街区:

self(std::move(self)) 创建类型为 std::unique_ptr<Session> 的 lambda 捕获(不是引用),并使用引用 self 进行初始化,引用 session_ptr 中的 lambda accept。通过 move-construction Session object 的所有权从 session_ptr 转移到 lambda 的成员。

然后将lambda传递给async_read_some,这将(因为lambda没有作为non-const左值引用传递)将lambda移动到内部存储中,以便以后可以异步调用.通过这一举措,Session object 的所有权也转移到 boost::asio 内部。

async_read_some returns 立即销毁 start 的所有局部变量和 accept 中的 lambda。但是 Session 的所有权已经转移,因此这里没有由于生命周期问题而导致的未定义行为。

将异步调用 lambda 的副本,它可能会再次调用 start,在这种情况下,Session 的所有权将转移给另一个 lambda 的成员和带有 [=55= 的 lambda ] 所有权将再次移至内部 boost::asio 存储。 lambda异步调用后,会被boost::asio销毁。然而此时,所有权已经转移。

Session object 最终被销毁,当 if(!errorCode) 失败并且拥有 std::unique_ptr<Session> 的 lambda 在其调用后被 boost::asio 销毁。

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