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

时间:2019-09-22 16:49:10

标签: c++ boost-asio shared-ptr 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));

这可以编译,但是我不知道这是否不是未定义的行为。由于我从对象移出时还对其调用了方法。

是UB吗?

如果是这样,我可以用以下方法修复它:

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

对吗?


(可选动机:)

传递对象以延长其寿命。它会存在于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;
}

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

1 个答案:

答案 0 :(得分:4)

对于您的第一个代码块:

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

它不会创建任何新的foo_p对象,托管std::unique_ptr<Foo>对象的所有权可以转移到该位置。没有移动构造或分配发生,并且Foo对象仍然被调用范围中的Foo破坏了。

因此,尽管您可以以可能导致体内未定义行为的方式使用引用foo_p,但此函数调用本身没有未定义行为的风险。

也许您打算让self成为self而不是std::unique_ptr<Foo>。在那种情况下,std::unique_ptr<Foo>&&不是参考,但是如果通过self进行调用,则托管对象Foo的所有权将通过移动构造传递到该对象,并且在std::move(p_foo)中的函数调用以及托管的foo_p->method(std::move(foo_p))

此替代变体本身是否可能是不确定的行为,取决于所使用的C ++标准版本。

在C ++ 17之前,允许编译器选择在评估Foo之前评估调用的参数(以及参数的关联移动结构)。这意味着foo_p->method可能已经从评估foo_p开始,从而导致未定义的行为。可以像您建议的方式进行修复。

从C ++ 17开始,可以保证在调用的任何参数之前对postfix-expression(此处为foo_p->method)进行求值,因此,调用本身不会成为问题。 (仍然会引起其他问题。)

对于后一种情况的详细说明:

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

然后,根据设计,参数的评估顺序不确定。因此, A)可能会由于初始化参数而将{_1}之前的unique_ptr foo_p移开。并且 B),它将 foo_p运行并使用初始化的this时移开。

但是 A)并不是问题,因为第8.2.2:4节符合预期:

  

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

(而且我们知道在 任何参数被求值之前,该对象已解决。)

B)无关紧要:({another question

  

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


第二段:

method创建类型为this(不是引用)的lambda捕获,该捕获使用引用self(std::move(self))初始化,引用{{1}中的lambda引用了std::unique_ptr<Session> }}。通过移动构造,self对象的所有权从session_ptr转移到lambda的成员。

然后将lambda传递到accept,这将(由于未将lambda作为非常量左值引用传递)将lambda移入内部存储,以便以后可以异步调用。通过此举,Session对象的所有权也转移到了boost :: asio内部。

session_ptr立即返回,因此async_read_some的所有局部变量和Session中的lambda被销毁。但是async_read_some的所有权已经转移,因此这里没有生命周期问题,因此没有未定义的行为。

将异步调用lambda的副本,该副本可能会再次调用start,在这种情况下,accept的所有权将被转移到另一个lambda的成员,而拥有Session所有权的lambda将再次移至内部boost :: asio存储。在异步调用lambda之后,它将被boost :: asio销毁。但是,在这一点上,所有权已经再次转移。

start失败并且拥有Session的lambda在调用之后被boost :: asio破坏时,Session对象最终被销毁。

因此,对于与Session的生命周期有关的未定义行为,我认为这种方法没有问题。如果您使用的是C ++ 17,则可以将if(!errorCode)放到std::unique_ptr<Session>参数中。