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
参数中删除 &&
。
我想将 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
参数中删除 &&
。