Title: Unique ptr move ownership to containing object's method
Tags: c++;boost-asio;shared-ptr;unique-ptr
Question: I'd like to move unique_ptr to its object's method:
class Foo {
void method(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Foo&gt;&amp;&amp; self) {
// this method now owns self
auto foo_p = st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0make_unique<Foo&gt;();
foo_p-&gt;method(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(foo_p));
This compiles, but I don't know if it is not Undefined behavior. Since I moved from the object when also calling a method on it.
Is it UB?
If it is, I could probably fix it with:
auto raw_foo_p = foo_p.get();
raw_foo_p-&gt;method(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(foo_p))
right?
(Optional Motivation:)
Pass the object around to extend its lifetime. It would live in a lambda until the lambda would be called asynchronously. (boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio)
Please, see Server75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0pt first and then Session75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0start.
You can see the original implementation used shared_ptr, but I don't see why would that be justified, since I only need one owner of my Session object.
Shared_ptr makes code more complex and it was hard for me to understand, when not familiar with shared_ptr.
#include <iostream&gt;
#include <memory&gt;
#include <utility&gt;
#include <boost/asio.hpp&gt;
using namespace boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0system;
using namespace boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio;
using boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0ip75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0tcp;
class Session /*: public st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0enable_shared_from_this<Session&gt;*/ {
public:
Session(tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0socket socket);
void start(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Session&gt;&amp;&amp; self);
private:
tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0socket socket_;
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string data_;
Session75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0Session(tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0socket socket) : socket_(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(socket))
void Session75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0start(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Session&gt;&amp;&amp; self)
// original code, replaced with unique_ptr
// auto self = shared_from_this();
socket_.async_read_some(buffer(data_), [this/*, self*/, self(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(self))]  (error_code errorCode, size_t) mutable {
if (!errorCode) {
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << "received: " << data_ << st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0endl;
start(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(self));
}
// if error code, this object gets automatically deleted as `self` enters end of the block
});
class Server {
public:
Server(io_context&amp; context);
private:
tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0ptor acceptor_;
void accept();
Server75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0Server(io_context&amp; context) : acceptor_(context, tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0ndpoint(tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0v4(), 8888))
accept();
void Server75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0pt()
acceptor_.async_accept([this](error_code errorCode, tcp75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0socket socket) {
if (!errorCode) {
// original code, replaced with unique_ptr
// st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0make_shared<Session&gt;(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(socket))-&gt;start();
auto session_ptr = st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0make_unique<Session&gt;(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(socket));
session_ptr-&gt;start(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(session_ptr));
}
accept();
});
int main()
boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0io_context context;
Server server(context);
context.run();
return 0;
compiles with:
g++ main.cpp -std=c++17 -lpthread -lboost_system
Comment: this method now owns self - no, it does not own. It's just a reference, no move performed. Yes, you may have UB, you call asynchronously the method just passing the reference, not transferring the ownership, depending on the variable life.

Comment: As you can see, the code hasn't become more simple and obvious after replacing shared_ptr with unique_ptr.

Comment: @S.M. He actually transfers ownership into the lambda via capture, which will be stored somewhere in the boost internals. So I don't see any issue with the asynchronous call.

Comment: @PaulSanders It is not actually a member of itself, the owning `unique_ptr` is held somewhere in unrelated storage, OP is merely using a member function to generate the required lambda. It could have been done in a free function just as well.

Comment: @S.M. I mean it owns the handler. The object should be on heap until a `unique_ptr` pointing to it gets destructed, right?

Comment: I can see no value in having a member of an object which is a `unique_ptr` to that object.  It just makes no sense.

Here is the accepted answer: For your first code block:
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Foo&gt;&amp;&amp; self is a reference and assigning it an argument st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(foo_p), where foo_p is a named st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Foo&gt; will only bind the reference self to foo_p, meaning that self will refer to foo_p in the calling scope.
It does not create any new st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Foo&gt; to which the ownership of the managed Foo object may be transferred. No move construction or assignment happens and the Foo object is still destroyed with the destruction of foo_p in the calling scope.
Therefore there is no risk of undefined behavior in this function call itself, although you could use the reference self in a way that could cause undefined behavior in the body.
Maybe you intended to have self be a st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Foo&gt; instead of st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Foo&gt;&amp;&amp;. In that case self would not be a reference, but an actual object to which ownership of the managed Foo would be transferred via move construction if called with st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(p_foo) and which would be destroyed after the function call in foo_p-&gt;method(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(foo_p)) together with the managed Foo.
Whether this alternative variant is in itself potentially undefined behavior depends on the C++ standard version in use.
Before C++17 the compiler was allowed to choose to evaluate the call's arguments (and the associated move construction of the parameter) before evaluating foo_p-&gt;method. This would mean, that foo_p could have already moved from when foo_p-&gt;method is evaluated, causing undefined behavior. This could be fixed similarly to how you propose to do it.
Since C++17 it is guaranteed that the postfix-expression (here foo_p-&gt;method) is evaluated before any of the arguments of the call are and therefore the call itself would not be a problem. (Still the body could cause other issues.)
In detail for the latter case:
foo_p-&gt;method is interpreted as (foo_p-&gt;operator-&gt;())-&gt;method, because st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr offers this operator-&gt;(). (foo_p-&gt;operator-&gt;()) will resolve to a pointer to the Foo object managed by the st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr. The last -&gt;method resolves to a member function method of that object. In C++17 this evaluation happens before any evaluation of the arguments to method and is therefore valid, because no move from foo_p has happened yet.
Then the evaluation order of the arguments is by design unspecified. So probably A) the unique_ptr foo_p could get moved from before this as an argument would be initialized. And B) it will get moved from by the time method runs and uses the initialized this.
But A) is not a problem, since § 8.2.2:4, as expected:
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,
(And we know this object was resolved before any argument was evaluated.)
And B) won't matter as: (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
For your second block:
self(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0move(self)) creates a lambda capture of type st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Session&gt; (not a reference) initialized with the reference self, which is referring to session_ptr in the lambda in accept. Via move-construction ownership of the Session object is transferred from session_ptr to the lambda's member.
The lambda is then passed to async_read_some, which will (because the lambda is not passed as non-const lvalue reference) move the lambda into internal storage, so that it can be called asynchronously later. With this move, the ownership of the Session object transfers to the boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio internals as well.
async_read_some returns immediately and so all local variables of start and the lambda in accept are destroyed. However the ownership of Session was already transferred and so there is no undefined behavior because of lifetime issues here.
Asynchronously the lambda's copy will be called, which may again call start, in which case the ownership of Session will be transferred to another lambda's member and the lambda with the Session ownership will again be moved to internal boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio storage. After the asynchronous call of the lambda, it will be destroyed by boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio. However at this point, again, ownership has already transferred.
The Session object is finally destroyed, when if(!errorCode) fails and the lambda with the owning st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Session&gt; is destroyed by boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0sio after its call.
Therefore I see no problem with this approach with regards to undefined behavior relating to Session's lifetime. If you are using C++17 then it would also be fine to drop the &amp;&amp; in the st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr<Session&gt;&amp;&amp; self parameter.
Comment for this answer: Perfect! I forgot `st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr&&` would only reference the object until I actually move it – assign it – to a value type object. Of course I intended `self` be a `st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0unique_ptr` in my first example!
Comment for this answer: I submitted an edit adding a more step-by-step explanation with debunked seemingly-pitfalls I found interesting and wanted to list them explicitly.


{'source': 'StackExchangeDataset', 'subset': 'stackoverflow.com', 'metadata': {'AnswerCount': 1, 'CommentCount': 6, 'ContentLicense': 'CC BY-SA 4.0', 'CreationDate': '2019:00:09/22/19', 'FavoriteCount': 1.0, 'PostTypeId': 1, 'Tags': '<c++><boost-asio><shared-ptr><unique-ptr>', 'Title': "Unique ptr move ownership to containing object's method", 'ViewCount': 496, 'id': 58051387, 'user_AboutMe': '', 'user_AccountId': 3526550.0, 'user_DisplayName': 'Adam', 'user_Reputation': 1613.0, 'user_Views': 136.0, 'user_id': 2947469.0}}