Уникальный ptr переместить владение на метод, содержащий объект - PullRequest
4 голосов
/ 22 сентября 2019

Я бы хотел переместить 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))

верно?


(Необязательная мотивация:)

Передайте объект, чтобы продлить срок его службы.Он будет жить в лямбде, пока лямбда не будет вызвана асинхронно.(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 Ответ

4 голосов
/ 23 сентября 2019

Для вашего первого блока кода:

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.Построение или назначение перемещения не происходит, и объект 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))вместе с управляемым Foo.

Является ли этот альтернативный вариант потенциально неопределенным поведением, зависит от используемой стандартной версии C ++.

До C ++ 17 компилятору было разрешено выбиратьоценить аргументы вызова (и связанную с ним конструкцию перемещения параметра) перед оценкой foo_p->method.Это будет означать, что foo_p мог уже сдвинуться с момента оценки foo_p->method, вызывая неопределенное поведение.Это можно исправить аналогично тому, как вы предлагаете это сделать.

Начиная с C ++ 17 гарантируется, что выражение postfix (здесь foo_p->method) будет вычислено до того, как любой из аргументов вызова будет ипоэтому сам вызов не будет проблемой.(Тем не менее, тело может вызвать другие проблемы.)

Подробно для последнего случая:

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.

Тогда порядок оценки аргументов является заданнымнеопределенные.Так что, вероятно, A) unique_ptr foo_p может быть перемещен из до this, поскольку аргумент будет инициализирован.И B) it будет перемещаться из ко времени выполнения method и использовать инициализированные this.

Но A) не является проблемой, так как § 8.2.2: 4, как и ожидалось:

Если функция является нестатической функцией-членом, параметр this функции должен бытьинициализируется указателем на объект вызова

(И мы знаем, что этот объект был разрешен до любого аргумента.)

И B) не имеет значения как: ( другой вопрос )

спецификация C ++ 11 гарантирует, что передача права собственности на объект от одного unique_ptr к другому unique_ptrне меняет местоположение самого объекта


Для вашего второго блока:

self(std::move(self)) создает лямбда-захват типа std::unique_ptr<Session> (не ссылка) инициализировансо ссылкой self, которая ссылается на session_ptr в лямбде в accept.Через перемещение-конструкцию право собственности на объект Session передается от session_ptr члену лямбды.

Затем лямбда передается в async_read_some, что будет (поскольку лямбда не передается как неconst lvalue reference) переместить лямбду во внутреннее хранилище, чтобы позже ее можно было вызывать асинхронно.После этого владения объект Session также переходит к внутренним элементам boost :: asio.

async_read_some возвращает немедленно, и поэтому все локальные переменные start и лямбда в accept уничтожаются.Однако владение Session уже было передано, и поэтому здесь нет неопределенного поведения из-за проблем с временем жизни.

Асинхронно будет вызываться лямбда-копия, которая может снова вызвать start, в этом случае владениеSession будет передан другому члену лямбды, а лямбда с владельцем Session снова будет перемещена во внутреннее хранилище boost :: asio.После асинхронного вызова лямбды он будет уничтожен boost :: asio.Однако в этот момент, опять же, право собственности уже передано.

Объект Session окончательно уничтожается, когда происходит сбой if(!errorCode) и лямбда с владельцем std::unique_ptr<Session> уничтожается при помощи boost :: asio после егоcall.

Поэтому я не вижу проблем с этим подходом в отношении неопределенного поведения, связанного с временем жизни Session.Если вы используете C ++ 17, тогда было бы неплохо удалить && в параметре std::unique_ptr<Session>&& self.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...