6

Can somebody explain why everybody passes std::unique_ptr by value instead of by rvalue reference?

From what I've observed, this required an additional move constructor to be invoked.

Here's an example of a class holding a "pointer". It takes 3 move-ctor calls to take it by value, versus 2 calls to take it by reference:

#include <memory>
#include <iostream>

class pointer {
public:
    pointer()
    { std::cerr << "ctor" << std::endl; }
    
    pointer(const pointer&)
    { std::cerr << "copy-ctor" << std::endl; }
    
    pointer& operator=(const pointer&)
    { std::cerr << "copy-assignment" << std::endl; return *this; }
    
    pointer(pointer&&)
    { std::cerr << "move-ctor" << std::endl; }
    
    pointer& operator=(pointer&&)
    { std::cerr << "move-assignment" << std::endl; return *this; }
    
    ~pointer()
    { std::cerr << "dtor" << std::endl; }
};

class A {
public:
    // V1
    A(pointer _ptr) : ptr(std::move(_ptr)) {}
        
    // V2
    A(pointer&& _ptr) : ptr(std::move(_ptr)) {}

private:
    pointer ptr;
};

int main() {
    // Three calls to move-ctor versus two calls if pass by rvalue reference
    auto ptr = pointer();
    A a(std::move(ptr));

    // Two calls to move-ctor always
    A a(pointer{});
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
banana36
  • 395
  • 3
  • 16
  • 1
    Your example doesn't compile at all: https://godbolt.org/z/oGr8cGshM Create a [mcve] – eerorika Apr 19 '22 at 20:21
  • You seem to be asking about `std::unique_ptr`, but your code doesn't contain a single mention of it. Furthermore there are uses of rvalue references to `std::unique_ptr` even in the standard library, see e.g. signature (13) here: https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr – fabian Apr 19 '22 at 20:27
  • C++ Core Guidelines have a bit on this in https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-consume so you can see what Bjarne and Herb think about it (spoiler, they think "Passing by value does generate one extra (cheap) move operation, but prefer simplicity and clarity first.") – Cubbi Apr 20 '22 at 18:07
  • @Cubbi While i agree with simplicity and clarity as primary principals, I also think you shouldn't presume move operations are present or cheap. If you *absolutely* want to guarantee only an rvalue is ever passed to your function, accept it by rvalue reference. In the specific case of `unique_ptr` its sort of moot which you choose since they can't be copied anyways, though. – Taekahn Apr 21 '22 at 19:59
  • I agree in general case. I was just thinking about `std::unique_ptr` like of something that can only be moved, that means you can't accept it by value avoiding casting to `&&`, then why to accept by copy if it *must* be moved anyway? – banana36 Apr 22 '22 at 20:04

3 Answers3

15

Passing a unique_ptr by reference, rvalue or otherwise, doesn't actual move anything, so you can't know by just looking at the function declaration if a move will happen.

Passing a unique_ptr by value on the other hand guarantees that the passed in pointer will be moved from, so without even have to look at the documentation you know calling that function releases you from the pointers' ownership.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 1
    Ooh, I missed that legit benefit to receiving by value. Up-voted. – ShadowRanger Apr 19 '22 at 20:27
  • @ShadowRanger Thanks. I'm a big fan of self documenting code, so I'm used to thinking about how parameter type and good names can help with that. – NathanOliver Apr 19 '22 at 20:35
  • I know that I can't by just looking at it predict what's going to be called. That's why I added debugging output in every c-tor. And I know as well that std::move is. The question what about passing it by value VS passing by rvalue reference. I don't get you point, unfortunately. – banana36 Apr 20 '22 at 06:33
  • @banana36 Why point is that you could have a function like `void foo(unique_ptr&& ptr) { ptr->do_something(); }` which doesn't actually transfer the ownership of the pointer to the function. When the function ends, the pointer at the cal site is still valid. If you instead had `void foo(unique_ptr ptr) ` it wouldn't matter what is in the body of the function, the only way to pass the `unique_ptr` into the function is to either implicitly move it by passing in a prvalue or explicitly move it by calling `move`. – NathanOliver Apr 20 '22 at 12:05
  • By passing by value, your API is guaranteeing that your function is taking ownership of the pointer – NathanOliver Apr 20 '22 at 12:05
  • Could you provide an example how can you pass a unique pointer without passing the ownership to function with declaration `foo(std::unique_ptr&&)`? – banana36 Apr 20 '22 at 13:50
  • 1
    @banana36 With that function, calling `foo(std::move(my_ptr_var))` wont actually pass ownership. `std::move` doesn't move anything, it just converts the type of the expression to an rvalue reference. If inside `foo` no move operation happened like my example, then `my_ptr_var` will not actually be moved from. If you had `foo(std::unique_ptr)` instead, then doing `foo(std::move(my_ptr_var))` will actually move `my_ptr_var`, as the compiler has to move construct the function parameter. This is why I like pass by value, it guarantees that the function is going to take the ownership. – NathanOliver Apr 20 '22 at 14:09
  • 1
    Here's you're writing this line of code: `foo(std::move(my_ptr_var))`. This means that you won't use that pointer anymore in this context. No matter if `foo` does or doesn't call move ctor/assigment from the object you've passed, it will be properly managed in its destructor either somewhere else in `foo`, or at the end of the current context. So, still don't get your point, unfortunately. – banana36 Apr 20 '22 at 15:17
  • While I do agree with the `pass by value and move idiom`, even if it generates an extra move, i have to say that while a function that accepts `bar&& val` doesn't explicitly take ownership, like it does when accepting by value, you _do_ know at the moment that function is called, that `val` is (as good as) an expiring value, and you're free to do what you want with it. Move it or otherwise. I think the only real excuse for using `bar&&` is when you want to say "I absolutely, positively, do not want any callers of this function to ever make a copy of the object and pass it to me",which is niche – Taekahn Apr 21 '22 at 20:12
6

For the same reason people pass int instead of const int&.

std::unique_ptr is just an RAII wrapper around a single pointer value, so moving it is just copying a single register width value, then zeroing the source. That's so trivial there's no real benefit to avoiding the move. After all, the cost to pass the reference (when not inlined) is the cost of passing a pointer too, so passing by reference can be less efficient (because if not inlined, it has to follow the reference to the real memory, then pull out the value from there; the top of the stack is likely in L1 cache, who knows if the place it's stored is?).

In practice, much of this will be inlined with optimizations enabled, and both approaches would get the same result. Passing by value is a good default when there's no benefit to passing by reference, so why not do it that way?

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • 1
    *"just copying a single register"* [Why can a T* be passed in register, but a unique_ptr cannot?](https://stackoverflow.com/q/58339165/2752075) ;_; – HolyBlackCat Apr 19 '22 at 21:49
2

why everybody passes the std::unique_ptr by value instead of rvalue reference?

It may be more common, but it's not "everybody".

The drawback of std::unique_ptr&& parameter is that it doesn't explicitly communicate to the caller whether the pointer will be moved from or not. It might always move, or it might depend on some condition. You would have to know the implementation or at least API documentation to know for sure. The corresponding benefit of std::unique_ptr parameter is that it alone tells the reader of the declaration that the function will take ownership of the pointer. For this reason, it may be a good choice to use std::unique_ptr parameter and probably part of the reason why it's more common.

The benefit of std::unique_ptr&& is avoiding the extra move. However, moving of a std::unique_ptr is a very fast operation. It's insignificant compared for example to the memory allocation itself. In most cases, it simply doesn't matter.

The difference between the two is fairly subtle. std::unique_ptr&& parameter may be considered in a case where you've measured the move to have significant cost. Which is not very common. Or in cases where your API may be used in cases where that cost could be significant. It's hard to prove that this won't ever happen if you're writing a public API, so it is a more likely argument to use.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Regarding the ownetrhip, as soon as unique ptr can only be moved (and not copied) - it means that the function always takes the ownership either if it's passed by value or rvalue reference, or am I wrong? – banana36 Apr 19 '22 at 20:49
  • @banana36 A unique pointer cannot be copied. The only way to create a `std::unique_ptr` parameter from another `std::unique_ptr` is for the caller to initialise the parameter by move. A `std::unique_ptr&&` can refer to an argument without necessarily moving from it. The difference in this necessary move is simultaneously what causes the extra move in one case, and leaves it ambiguous whether a move happens in the other case. – eerorika Apr 19 '22 at 20:52
  • 4
    I still don't get it. If you pass `std::unique_ptr` to function `foo(std::unique_ptr&&);` you're only able to pass it either as a return value from somewhere or by explicitly calling `std::move` on it. Could you please give an example of ambiguous call of such function? – banana36 Apr 20 '22 at 06:30