Does Std::Bind Work with Move-Only Types in General, and Std::Unique_Ptr in Particular

Does std::bind work with move-only types in general, and std::unique_ptr in particular?

std::bind works fine with move-only types. However it creates a move-only functor in the process. std::function requires a copy constructible functor. It sounds like boost::asio does too.

When you call the move-only bind functor, it will pass its bound arguments as lvalues to the target operator(). So if one of your bound arguments is move-only, the target operator() must take that argument by (possibly const) lvalue reference.

Why do I need to move `std::unique_ptr`

I don't understand why I need to call std::move to make the function work.

Because the corresponding constructor of std::unique_ptr has a parameter of rvalue reference type:

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept;

See documentation for details: https://en.cppreference.com/w/cpp/memory/unique_ptr/unique_ptr

Since rvalue references cannot bind lvalues, consequently, you cannot use b (which is lvalue) as an argument of this constructor.

If you wonder why b is treated as lvalue in the return statement, see, for example: Why Structured Bindings disable both RVO and move on return statement? In short, b is not a variable with automatic storage duration, but a reference to a pair element instead.

The error message basically just says that the compiler could not find any viable converting constructor, therefore, it "cannot convert...".

By wrapping b with std::move call, you are creating an expression that refers to the very same object as b, but its category is rvalue. Which may be bound with that constructor parameter.

Unique ptr move ownership to containing object's method

For your first code block:

std::unique_ptr<Foo>&& self is a reference and assigning it an argument std::move(foo_p), where foo_p is a named std::unique_ptr<Foo> 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 std::unique_ptr<Foo> 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 std::unique_ptr<Foo> instead of std::unique_ptr<Foo>&&. 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 std::move(p_foo) and which would be destroyed after the function call in foo_p->method(std::move(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->method. This would mean, that foo_p could have already moved from when foo_p->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->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->method is interpreted as (foo_p->operator->())->method, because std::unique_ptr offers this operator->(). (foo_p->operator->()) will resolve to a pointer to the Foo object managed by the std::unique_ptr. The last ->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(std::move(self)) creates a lambda capture of type std::unique_ptr<Session> (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 boost::asio 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 boost::asio storage. After the asynchronous call of the lambda, it will be destroyed by boost::asio. 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 std::unique_ptr<Session> is destroyed by boost::asio 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 && in the std::unique_ptr<Session>&& self parameter.

std::function with unique_ptr argument

The code below compiles fine using gcc 4.8. You will notice that
if "g" is not called with move (that converts the lvalue to an rvalue),
the code fails to compile. As mentioned earlier, the bind succeeds because
the failure only happens when operator()( ... ) is called, because of the
fact that unique_ptr is not copyable. The call "f" is permissible, as
shared_ptr has a copy constructor.

#include <functional>
#include <memory>

void foo1( std::shared_ptr<int> ){}
void foo2( std::unique_ptr<int> ){}

int main()
{
using namespace std::placeholders;

std::function<void(std::shared_ptr<int>)> f = std::bind( foo1, _1 );
std::function<void(std::unique_ptr<int>)> g = std::bind( foo2, _1 );

std::unique_ptr<int> i( new int(5) );
g( move( i ) ); //Requires the move

std::shared_ptr<int> j( new int(5) );
f( j ); //Works fine without the move
return 0;
}

How does std::bind Results in calling the Copy Constructor Several Times

First of all, according to the rules for move constructors, no implicit move constructor is defined for the class NAME. Further, from the Notes here:

If only the copy constructor is provided, all argument categories
select it (as long as it takes a reference to const, since rvalues can
bind to const references), which makes copying the fallback for
moving, when moving is unavailable.

So, whenever you use std::move you are eventually calling a copy constructor. This explains why Version 4 (respectively, Version 2) has an additional call to the copy constructor compared to Version 3 (respectively, Version 1).

Let us see the remaining copy constructors.

As you correctly pointed out, a copy constructor is called from passing second argument of std::bind. This account for the first call in all versions.

When you declare

 std::function<void ()> callable = std::bind(&NAME::f, n);

you are calling the constructor of std::function, passing a single parameter std::bind(&NAME::f, n) which is then, again, copied. This account for the second call of copy constructor of version 1, and the third call in version 2. Notice that mandatory copy elision does not apply here, because you are not passing a std::function object.

Finally, when you use

auto callable = std::bind(...)

you are declaring a variable which is of an unnamed type, and contains the result of the call to std::bind.
No copy is involved in the declaration. This is why version 3 has one less call to copy constructor compared to version 1.

Answers to the additional questions

1.

The types of callable_1 and callable_2 are different. callable_2 is a std::function object, while callable_1 is an unspecified type, the result of std::bind. Also, it is not a lamda. To see this, you can run a code like

   auto callable_1 = std::bind(&NAME::f, n);
std::function<void ()> callable_2 = std::bind(&NAME::f, n);
// a generic lambda
auto callable_3 = [&]() { n.f(); };
std::cout << std::boolalpha;
std::cout << std::is_bind_expression<decltype(callable_1)>::value << std::endl;
std::cout << std::is_bind_expression<decltype(callable_2)>::value << std::endl;
std::cout << std::is_bind_expression<decltype(callable_3)>::value << std::endl;

See it live on Coliru.

2.

As noted by @RemyLebeau, a strict interpretation of the Notes in the reference of std::bind

As described in Callable, when invoking a pointer to non-static member
function or pointer to non-static data member, the first argument has
to be a reference or pointer (including, possibly, smart pointer such
as std::shared_ptr and std::unique_ptr) to an object whose member will
be accessed.

would suggest that the code must be called with &n and a call with n would be illegal.

However, a call to operator() results in a std::invoke. From the reference of std::invoke we read (little reformatting by me):

If f is a pointer to member function of class T:

a) If std::is_base_of<T, std::decay_t<decltype(t1)>>::value is true, then INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.*f)(t2,
..., tN)

b) If std::decay_t<decltype(t1)> is a specialization of
std::reference_wrapper, then INVOKE(f, t1, t2, ..., tN) is equivalent
to (t1.get().*f)(t2, ..., tN)

c) If t1 does not satisfy the previous
items, then INVOKE(f, t1, t2, ..., tN) is equivalent to ((*t1).*f)(t2,
..., tN).

According to this, calling std::bind with n (case a)) or &n (case c)) should be equivalent (apart from the additional copy if you use n), because std::decay_t<decltype(n)> gives NAME and std::is_base_of<NAME, NAME>::value is true (see reference for std::is_base_of).
Passing ref(n) corresponds to the case b), so again it should be correct and equivalent to the other cases (apart from the copies discussed above).

3.

Notice that cref gives you a reference wrapper to const NAME&. So you will not be able to call callable because NAME::f is not a const member function. In fact, if you add a callable(); the code does not compile.

Apart from this issue, if you use instead std::ref or if NAME::f is const, I do not see a fundamental difference between auto callable = [&n]{n.f()}; and auto callable = std::bind(&NAME::f, ref(n));. With these regards, notice that:

The arguments to bind are copied or moved, and are never passed by
reference unless wrapped in std::ref or std::cref.

Personally, I find the lambda syntax much clearer.

4.

From the reference for std::bind, under operator() we read

If the stored argument arg is of type T, for which
std::is_placeholder::value != 0 (meaning, a placeholder such as
std::placeholders::_1, _2, _3, ... was used as the argument to the
initial call to bind), then the argument indicated by the placeholder
(u1 for _1, u2 for _2, etc) is passed to the invokable object: the
argument vn in the std::invoke call above is std::forward(uj) and
the corresponding type Vn in the same call is Uj&&.

Thus, the effect of using placeholders falls back to the cases of question 1. The code indeed compiles with different compilers.



Related Topics



Leave a reply



Submit