How do I pass a unique_ptr argument to a constructor or a function?

前端 未结 7 1644
挽巷
挽巷 2020-11-22 06:16

I\'m new to move semantics in C++11 and I don\'t know very well how to handle unique_ptr parameters in constructors or functions. Consider this class referenc

相关标签:
7条回答
  • 2020-11-22 06:50

    tl;dr: Do not use unique_ptr's like that.

    I believe you're making a terrible mess - for those who will need to read your code, maintain it, and probably those who need to use it.

    1. Only take unique_ptr constructor parameters if you have publicly-exposed unique_ptr members.

    unique_ptrs wrap raw pointers for ownership & lifetime management. They're great for localized use - not good, nor in fact intended, for interfacing. Wanna interface? Document your new class as ownership-taking, and let it get the raw resource; or perhaps, in the case of pointers, use owner<T*> as suggested in the Core Guidelines.

    Only if the purpose of your class is to hold unique_ptr's, and have others use those unique_ptr's as such - only then is it reasonable for your constructor or methods to take them.

    1. Don't expose the fact that you use unique_ptrs internally

    Using unique_ptr for list nodes is very much an implementation detail. Actually, even the fact that you're letting users of your list-like mechanism just use the bare list node directly - constructing it themselves and giving it to you - is not a good idea IMHO. I should not need to form a new list-node-which-is-also-a-list to add something to your list - I should just pass the payload - by value, by const lvalue ref and/or by rvalue ref. Then you deal with it. And for splicing lists - again, value, const lvalue and/or rvalue.

    0 讨论(0)
  • 2020-11-22 06:52

    Here are the possible ways to take a unique pointer as an argument, as well as their associated meaning.

    (A) By Value

    Base(std::unique_ptr<Base> n)
      : next(std::move(n)) {}
    

    In order for the user to call this, they must do one of the following:

    Base newBase(std::move(nextBase));
    Base fromTemp(std::unique_ptr<Base>(new Base(...));
    

    To take a unique pointer by value means that you are transferring ownership of the pointer to the function/object/etc in question. After newBase is constructed, nextBase is guaranteed to be empty. You don't own the object, and you don't even have a pointer to it anymore. It's gone.

    This is ensured because we take the parameter by value. std::move doesn't actually move anything; it's just a fancy cast. std::move(nextBase) returns a Base&& that is an r-value reference to nextBase. That's all it does.

    Because Base::Base(std::unique_ptr<Base> n) takes its argument by value rather than by r-value reference, C++ will automatically construct a temporary for us. It creates a std::unique_ptr<Base> from the Base&& that we gave the function via std::move(nextBase). It is the construction of this temporary that actually moves the value from nextBase into the function argument n.

    (B) By non-const l-value reference

    Base(std::unique_ptr<Base> &n)
      : next(std::move(n)) {}
    

    This has to be called on an actual l-value (a named variable). It cannot be called with a temporary like this:

    Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.
    

    The meaning of this is the same as the meaning of any other use of non-const references: the function may or may not claim ownership of the pointer. Given this code:

    Base newBase(nextBase);
    

    There is no guarantee that nextBase is empty. It may be empty; it may not. It really depends on what Base::Base(std::unique_ptr<Base> &n) wants to do. Because of that, it's not very evident just from the function signature what's going to happen; you have to read the implementation (or associated documentation).

    Because of that, I wouldn't suggest this as an interface.

    (C) By const l-value reference

    Base(std::unique_ptr<Base> const &n);
    

    I don't show an implementation, because you cannot move from a const&. By passing a const&, you are saying that the function can access the Base via the pointer, but it cannot store it anywhere. It cannot claim ownership of it.

    This can be useful. Not necessarily for your specific case, but it's always good to be able to hand someone a pointer and know that they cannot (without breaking rules of C++, like no casting away const) claim ownership of it. They can't store it. They can pass it to others, but those others have to abide by the same rules.

    (D) By r-value reference

    Base(std::unique_ptr<Base> &&n)
      : next(std::move(n)) {}
    

    This is more or less identical to the "by non-const l-value reference" case. The differences are two things.

    1. You can pass a temporary:

      Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
      
    2. You must use std::move when passing non-temporary arguments.

    The latter is really the problem. If you see this line:

    Base newBase(std::move(nextBase));
    

    You have a reasonable expectation that, after this line completes, nextBase should be empty. It should have been moved from. After all, you have that std::move sitting there, telling you that movement has occurred.

    The problem is that it hasn't. It is not guaranteed to have been moved from. It may have been moved from, but you will only know by looking at the source code. You cannot tell just from the function signature.

    Recommendations

    • (A) By Value: If you mean for a function to claim ownership of a unique_ptr, take it by value.
    • (C) By const l-value reference: If you mean for a function to simply use the unique_ptr for the duration of that function's execution, take it by const&. Alternatively, pass a & or const& to the actual type pointed to, rather than using a unique_ptr.
    • (D) By r-value reference: If a function may or may not claim ownership (depending on internal code paths), then take it by &&. But I strongly advise against doing this whenever possible.

    How to manipulate unique_ptr

    You cannot copy a unique_ptr. You can only move it. The proper way to do this is with the std::move standard library function.

    If you take a unique_ptr by value, you can move from it freely. But movement doesn't actually happen because of std::move. Take the following statement:

    std::unique_ptr<Base> newPtr(std::move(oldPtr));
    

    This is really two statements:

    std::unique_ptr<Base> &&temporary = std::move(oldPtr);
    std::unique_ptr<Base> newPtr(temporary);
    

    (note: The above code does not technically compile, since non-temporary r-value references are not actually r-values. It is here for demo purposes only).

    The temporary is just an r-value reference to oldPtr. It is in the constructor of newPtr where the movement happens. unique_ptr's move constructor (a constructor that takes a && to itself) is what does the actual movement.

    If you have a unique_ptr value and you want to store it somewhere, you must use std::move to do the storage.

    0 讨论(0)
  • 2020-11-22 06:53
    Base(Base::UPtr n):next(std::move(n)) {}
    

    should be much better as

    Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}
    

    and

    void setNext(Base::UPtr n)
    

    should be

    void setNext(Base::UPtr&& n)
    

    with same body.

    And ... what is evt in handle() ??

    0 讨论(0)
  • 2020-11-22 06:56

    To the top voted answer. I prefer passing by rvalue reference.

    I understand what's the problem about passing by rvalue reference may cause. But let's divide this problem to two sides:

    • for caller:

    I must write code Base newBase(std::move(<lvalue>)) or Base newBase(<rvalue>).

    • for callee:

    Library author should guarantee it will actually move the unique_ptr to initialize member if it want own the ownership.

    That's all.

    If you pass by rvalue reference, it will only invoke one "move" instruction, but if pass by value, it's two.

    Yep, if library author is not expert about this, he may not move unique_ptr to initialize member, but it's the problem of author, not you. Whatever it pass by value or rvalue reference, your code is same!

    If you are writing a library, now you know you should guarantee it, so just do it, passing by rvalue reference is a better choice than value. Client who use you library will just write same code.

    Now, for your question. How do I pass a unique_ptr argument to a constructor or a function?

    You know what's the best choice.

    http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

    0 讨论(0)
  • 2020-11-22 06:59

    Edit: This answer is wrong, even though, strictly speaking, the code works. I'm only leaving it here because the discussion under it is too useful. This other answer is the best answer given at the time I last edited this: How do I pass a unique_ptr argument to a constructor or a function?

    The basic idea of ::std::move is that people who are passing you the unique_ptr should be using it to express the knowledge that they know the unique_ptr they're passing in will lose ownership.

    This means you should be using an rvalue reference to a unique_ptr in your methods, not a unique_ptr itself. This won't work anyway because passing in a plain old unique_ptr would require making a copy, and that's explicitly forbidden in the interface for unique_ptr. Interestingly enough, using a named rvalue reference turns it back into an lvalue again, so you need to use ::std::move inside your methods as well.

    This means your two methods should look like this:

    Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability
    
    void setNext(Base::UPtr &&n) { next = ::std::move(n); }
    

    Then people using the methods would do this:

    Base::UPtr objptr{ new Base; }
    Base::UPtr objptr2{ new Base; }
    Base fred(::std::move(objptr)); // objptr now loses ownership
    fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership
    

    As you see, the ::std::move expresses that the pointer is going to lose ownership at the point where it's most relevant and helpful to know. If this happened invisibly, it would be very confusing for people using your class to have objptr suddenly lose ownership for no readily apparent reason.

    0 讨论(0)
  • 2020-11-22 07:08

    Yes you have to if you take the unique_ptr by value in the constructor. Explicity is a nice thing. Since unique_ptr is uncopyable (private copy ctor), what you wrote should give you a compiler error.

    0 讨论(0)
提交回复
热议问题