cod*_*nk1 377 c++ arguments unique-ptr c++11
我是新手在C++ 11中移动语义,我不太清楚如何处理unique_ptr构造函数或函数中的参数.考虑这个引用自身的类:
#include <memory>
class Base
{
public:
typedef unique_ptr<Base> UPtr;
Base(){}
Base(Base::UPtr n):next(std::move(n)){}
virtual ~Base(){}
void setNext(Base::UPtr n)
{
next = std::move(n);
}
protected :
Base::UPtr next;
};
Run Code Online (Sandbox Code Playgroud)
这是我应该如何编写unique_ptr参数的函数?
我需要std::move在调用代码中使用吗?
Base::UPtr b1;
Base::UPtr b2(new Base());
b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
Run Code Online (Sandbox Code Playgroud)
Nic*_*las 801
以下是将唯一指针作为参数的可能方法,以及它们的相关含义.
Base(std::unique_ptr<Base> n)
: next(std::move(n)) {}
Run Code Online (Sandbox Code Playgroud)
为了让用户调用它,他们必须执行以下操作之一:
Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));
Run Code Online (Sandbox Code Playgroud)
按值获取唯一指针意味着您将指针的所有权转移到相关的函数/对象/等.newBase构建完成后,nextBase保证是空的.你不拥有这个对象,你甚至没有指向它的指针.没了.
这是确保的,因为我们按值获取参数.std::move实际上并没有任何动静 ; 这只是一个花哨的演员.std::move(nextBase)返回一个Base&&r值引用nextBase.就是这样.
因为Base::Base(std::unique_ptr<Base> n)它的参数是按值而不是通过r值引用,所以C++会自动为我们构造一个临时值.它创建了一个std::unique_ptr<Base>从Base&&我们通过给函数std::move(nextBase).正是这个临时的构造实际上将值移动nextBase到函数参数中n.
Base(std::unique_ptr<Base> &n)
: next(std::move(n)) {}
Run Code Online (Sandbox Code Playgroud)
必须在实际的l值(命名变量)上调用它.它不能像这样临时调用:
Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.
Run Code Online (Sandbox Code Playgroud)
其含义与非const引用的任何其他用法的含义相同:该函数可能会或可能不会声明指针的所有权.鉴于此代码:
Base newBase(nextBase);
Run Code Online (Sandbox Code Playgroud)
无法保证nextBase是空的.它可能是空的; 它可能不会.这真的取决于Base::Base(std::unique_ptr<Base> &n)想做什么.正因为如此,从功能签名中发现的事情并不是很明显.你必须阅读实现(或相关文档).
因此,我不建议将其作为界面.
Base(std::unique_ptr<Base> const &n);
Run Code Online (Sandbox Code Playgroud)
我没有显示实现,因为你不能从一个实现const&.通过传递a const&,你说该函数可以Base通过指针访问它,但它无法将它存储在任何地方.它不能声称拥有它.
这可能很有用.不一定适合你的特定情况,但是能够交出一个指针并且知道他们不能(不破坏C++的规则,如同没有丢弃const)声明对它的所有权总是好的.他们无法存储它.他们可以将它传递给其他人,但其他人必须遵守相同的规则.
Base(std::unique_ptr<Base> &&n)
: next(std::move(n)) {}
Run Code Online (Sandbox Code Playgroud)
这与"非常量l值参考"情况或多或少相同.差异是两件事.
你可以传递一个临时的:
Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
Run Code Online (Sandbox Code Playgroud)您必须使用std::move通过非暂时性的参数时.
后者确实是问题所在.如果你看到这一行:
Base newBase(std::move(nextBase));
Run Code Online (Sandbox Code Playgroud)
您有一个合理的期望,在此行完成后,nextBase应为空.它本应该被移走.毕竟,你std::move坐在那里,告诉你运动已经发生.
问题是它没有.不能保证不会被移走.它可能已被移除,但您只能通过查看源代码来了解.你无法从功能签名中分辨出来.
unique_ptr,则按价值取值.unique_ptr函数执行的持续时间内使用它,那就把它拿走const&.或者,将a &或者传递给const&指向的实际类型,而不是使用a unique_ptr.&&.但我强烈建议尽可能不要这样做.你不能复制unique_ptr.你只能移动它.正确的方法是使用std::move标准库函数.
如果你取一个unique_ptrby值,你可以自由地移动它.但是运动实际上并不是因为std::move.请采取以下声明:
std::unique_ptr<Base> newPtr(std::move(oldPtr));
Run Code Online (Sandbox Code Playgroud)
这真的是两个陈述:
std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);
Run Code Online (Sandbox Code Playgroud)
(注意:上面的代码在技术上没有编译,因为非临时r值引用实际上不是r值.它仅用于演示目的).
这temporary只是一个r值参考oldPtr.它是运动发生的构造者newPtr.unique_ptr移动构造函数(一个&&自带的构造函数)是实际运动的内容.
如果您有unique_ptr值并且想要将其存储在某个位置,则必须使用std::move该存储.
Mar*_*wen 53
Let me try to state the different viable modes of passing pointers around to objects whose memory is managed by an instance of the std::unique_ptr class template; it also applies to the the older std::auto_ptr class template (which I believe allows all uses that unique pointer does, but for which in addition modifiable lvalues will be accepted where rvalues are expected, without having to invoke std::move), and to some extent also to std::shared_ptr.
As a concrete example for the discussion I will consider the following simple list type
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
Run Code Online (Sandbox Code Playgroud)
Instances of such list (which cannot be allowed to share parts with other instances or be circular) are entirely owned by whoever holds the initial list pointer. If client code knows that the list it stores will never be empty, it may also choose to store the first node directly rather than a list.
No destructor for node needs to be defined: since the destructors for its fields are automatically called, the whole list will be recursively deleted by the smart pointer destructor once the lifetime of initial pointer or node ends.
This recursive type gives the occasion to discuss some cases that are less visible in the case of a smart pointer to plain data. Also the functions themselves occasionally provide (recursively) an example of client code as well. The typedef for list is of course biased towards unique_ptr, but the definition could be changed to use auto_ptr or shared_ptr instead without much need to change to what is said below (notably concerning exception safety being assured without the need to write destructors).
如果您的功能与所有权无关,那么这是首选方法:根本不要使用智能指针.在这种情况下,您的函数不需要担心谁拥有所指向的对象,或者通过什么方式管理所有权,因此传递原始指针既非常安全,也是最灵活的形式,因为无论所有权如何,客户端都可以始终生成一个原始指针(通过调用get方法或从地址运算符&).
例如,计算此类列表长度的函数不应该是一个list参数,而是一个原始指针:
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
Run Code Online (Sandbox Code Playgroud)
A client that holds a variable list head can call this function as length(head.get()),
while a client that has chosen instead to store a node n representing a non-empty list can call length(&n).
If the pointer is guaranteed to be non null (which is not the case here since lists may be empty) one might prefer to pass a reference rather than a pointer. It might be a pointer/reference to non-const if the function needs to update the contents of the node(s), without adding or removing any of them (the latter would involve ownership).
An interesting case that falls in the mode 0 category is making a (deep) copy of the list; while a function doing this must of course transfer ownership of the copy it creates, it is not concerned with the ownership of the list it is copying. So it could be defined as follows:
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
Run Code Online (Sandbox Code Playgroud)
This code merits a close look, both for the question as to why it compiles at all (the result of the recursive call to copy in the initialiser list binds to the rvalue reference argument in the move constructor of unique_ptr<node>, a.k.a. list, when initialising the next field of the generated node), and for the question as to why it is exception-safe (if during the recursive allocation process memory runs out and some call of new throws std::bad_alloc, then at that time a pointer to the partly constructed list is held anonymously in a temporary of type list created for the initialiser list, and its destructor will clean up that partial list). By the way one should resist the temptation to replace (as I initially did) the second nullptr by p毕竟在这一点上已知为null:一个人不能从(原始)指针构造一个智能指针到常量,即使它已知为null.
A function that takes a smart pointer value as argument takes possession of the object pointed to right away: the smart pointer that the caller held (whether in a named variable or an anonymous temporary) is copied into the argument value at function entrance and the caller's pointer has become null (in the case of a temporary the copy might have been elided, but in any case the caller has lost access to the pointed to object). I would like to call this mode call by cash: caller pays up front for the service called, and can have no illusions about ownership after the call. To make this clear, the language rules require the caller to wrap the argument in std::move如果智能指针保存在变量中(从技术上讲,如果参数是左值); 在这种情况下(但不适用于下面的模式3),此函数按其名称建议,即将值从变量移动到临时,将变量保留为null.
For cases where the called function unconditionally takes ownership of (pilfers) the pointed-to object, this mode used with std::unique_ptr or std::auto_ptr is a good way of passing a pointer together with its ownership, which avoids any risk of memory leaks. Nonetheless I think that there are only very few situations where mode 3 below is not to be preferred (ever so slightly) over mode 1. For this reason I shall provide no usage examples of this mode. (But see the reversed example of mode 3 below, where it is remarked that mode 1 would do at least as well.) If the function takes more arguments than just this pointer, it may happen that there is in addition a technical reason to avoid mode 1 (with std::unique_ptr or std::auto_ptr): since an actual move operation takes place while passing a pointer variable p通过表达式std::move(p),不能假设p在评估其他参数(评估的顺序未指定)时保持有用的值,这可能导致细微的错误; 相反,使用模式3可确保p在函数调用之前不会发生移动,因此其他参数可以安全地访问值p.
When used with std::shared_ptr, this mode is interesting in that with a single function definition it allows the caller to choose whether to keep a sharing copy of the pointer for itself while creating a new sharing copy to be used by the function (this happens when an lvalue argument is provided; the copy constructor for shared pointers used at the call increases the reference count), or to just give the function a copy of the pointer without retaining one or touching the reference count (this happens when a rvalue argument is provided, possibly an lvalue wrapped in a call of std::move). For instance
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
Run Code Online (Sandbox Code Playgroud)
The same could be achieved by separately defining void f(const std::shared_ptr<X>& x) (for the lvalue case) and void f(std::shared_ptr<X>&& x) (for the rvalue case), with function bodies differing only in that the first version invokes copy semantics (using copy construction/assignment when using x) but the second version move semantics (writing std::move(x) instead, as in the example code). So for shared pointers, mode 1 can be useful to avoid some code duplication.
Here the function just requires having a modifiable reference to the smart pointer, but gives no indication of what it will do with it. I would like to call this method call by card: caller ensures payment by giving a credit card number. The reference can be used to take ownership of the pointed-to object, but it does not have to. This mode requires providing a modifiable lvalue argument, corresponding to the fact that the desired effect of the function may include leaving a useful value in the argument variable. A caller with an rvalue expression that it wishes to pass to such a function would be forced to store it in a named variable to be able to make the call, since the language only provides implicit conversion to a constant lvalue reference (referring to a temporary) from an rvalue. (Unlike the opposite situation handled by std::move, a cast from Y&& to Y&, with Y the smart pointer type, is not possible; nonetheless this conversion could be obtained by a simple template function if really desired; see /sf/answers/1740786351/). For the case where the called function intends to unconditionally take ownership of the object, stealing from the argument, the obligation to provide an lvalue argument is giving the wrong signal: the variable will have no useful value after the call. Therefore mode 3, which gives identical possibilities inside our function but asks callers to provide an rvalue, should be preferred for such usage.
However there is a valid use case for mode 2, namely functions that may modify the pointer, or the object pointed to in a way that involves ownership. For instance, a function that prefixes a node to a list provides an example of such use:
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
Run Code Online (Sandbox Code Playgroud)
Clearly it would be undesirable here to force callers to use std::move, since their smart pointer still owns a well defined and non-empty list after the call, though a different one than before.
Again it is interesting to observe what happens if the prepend call fails for lack of free memory. Then the new call will throw std::bad_alloc; at this point in time, since no node could be allocated, it is certain that the passed rvalue reference (mode 3) from std::move(l) cannot yet have been pilfered, as that would be done to construct the next field of the node that failed to be allocated. So the original smart pointer l still holds the original list when the error is thrown; that list will either be properly destroyed by the smart pointer destructor, or in case l should survive thanks to a sufficiently early catch clause, it will still hold the original list.
That was a constructive example; with a wink to this question one can also give the more destructive example of removing the first node containing a given value, if any:
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
Run Code Online (Sandbox Code Playgroud)
Again the correctness is quite subtle here. Notably, in the final statement the pointer (*p)->next held inside the node to be removed is unlinked (by release, which returns the pointer but makes the original null) before reset (implicitly) destroys that node (when it destroys the old value held by p), ensuring that one and only one node is destroyed at that time. (In the alternative form mentioned in the comment, this timing would be left to the internals of the implementation of the move-assignment operator of the std::unique_ptr instance list; the standard says 20.7.1.2.3;2 that this operator should act "as if by calling reset(u.release())", whence the timing should be safe here too.)
Note that prepend and remove_first cannot be called by clients who store a local node variable for an always non-empty list, and rightly so since the implementations given could not work for such cases.
This is the preferred mode to use when simply taking ownership of the pointer. I would like to call this method call by check: caller must accept relinquishing ownership, as if providing cash, by signing the check, but the actual withdrawal is postponed until the called function actually pilfers the pointer (exactly as it would when using mode 2). The "signing of the check" concretely means callers have to wrap an argument in std::move (as in mode 1) if it is an lvalue (if it is an rvalue, the "giving up ownership" part is obvious and requires no separate code).
Note that technically mode 3 behaves exactly as mode 2, so the called function does not have to assume ownership; however I would insist that if there is any uncertainty about ownership transfer (in normal usage), mode 2 should be preferred to mode 3, so that using mode 3 is implicitly a signal to callers that they are giving up ownership. One might retort that only mode 1 argument passing really signals forced loss of ownership to callers. But if a client has any doubts about intentions of the called function, she is supposed to know the specifications of the function being called, which should remove any doubt.
It is surprisingly difficult to find a typical example involving our list type that uses mode 3 argument passing. Moving a list b to the end of another list a is a typical example; however a (which survives and holds the result of the operation) is better passed using mode 2:
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
Run Code Online (Sandbox Code Playgroud)
A pure example of mode 3 argument passing is the following that takes a list (and its ownership), and returns a list containing the identical nodes in reverse order.
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
Run Code Online (Sandbox Code Playgroud)
This function might be called as in l = reversed(std::move(l)); to reverse the list into itself, but the reversed list can also be used differently.
Here the argument is immediately moved to a local variable for efficiency (one could have used the parameter l directly in the place of p, but then accessing it each time would involve an extra level of indirection); hence the difference with mode 1 argument passing is minimal. In fact using that mode, the argument could have served directly as local variable, thus avoiding that initial move; this is just an instance of the general principle that if an argument passed by reference only serves to initialise a local variable, one might just as well pass it by value instead and use the parameter as local variable.
Using mode 3 appears to be advocated by the standard, as witnessed by the fact that all provided library functions that transfer ownership of smart pointers using mode 3. A particular convincing case in point is the constructor std::shared_ptr<T>(auto_ptr<T>&& p). That constructor used (in std::tr1) to take a modifiable lvalue reference (just like the auto_ptr<T>& copy constructor), and could therefore be called with an auto_ptr<T> lvalue p as in std::shared_ptr<T> q(p), after which p has been reset to null. Due to the change from mode 2 to 3 in argument passing, this old code must now be rewritten to std::shared_ptr<T> q(std::move(p)) and will then continue to work. I understand that the committee did not like the mode 2 here, but they had the option of changing to mode 1, by defining std::shared_ptr<T>(auto_ptr<T> p) instead, they could have ensured that old code works without modification, because (unlike unique-pointers) auto-pointers can be silently dereferenced to a value (the pointer object itself being reset to null in the process). Apparently the committee so much preferred advocating mode 3 over mode 1, that they chose to actively break existing code rather than to use mode 1 even for an already deprecated usage.
Mode 1 is perfectly usable in many cases, and might be preferred over mode 3 in cases where assuming ownership would otherwise takes the form of moving the smart pointer to a local variable as in the reversed example above. However, I can see two reasons to prefer mode 3 in the more general case:
It is slightly more efficient to pass a reference than to create a temporary and nix the old pointer (handling cash is somewhat laborious); in some scenarios the pointer may be passed several times unchanged to another function before it is actually pilfered. Such passing will generally require writing std::move (unless mode 2 is used), but note that this is just a cast that does not actually do anything (in particular no dereferencing), so it has zero cost attached.
Should it be conceivable that anything throws an exception between the start of the function call and the point where it (or some contained call) actually moves the pointed-to object into another data structure (and this exception is not already caught inside the function itself), then when using mode 1, the object referred to by the smart pointer will be destroyed before a catch clause can handle the exception (because the function parameter was destructed during stack unwinding), but not so when using mode 3. The latter gives the caller has the option to recover the data of the object in such cases (by catching the exception). Note that mode 1 here does not cause a memory leak, but may lead to an unrecoverable loss of data for the program, which might be undesirable as well.
To conclude a word about returning a smart pointer, presumably pointing to an object created for use by the caller. This is not really a case comparable with passing pointers into functions, but for completeness I would like to insist that in such cases always return by value (and don't use std::move in the return statement). Nobody wants to get a reference to a pointer that probably has just been nixed.
unique_ptr是的,如果您在构造函数中按值获取,则必须这样做。明确是一件好事。由于unique_ptr是不可复制的(私有复制者),因此您编写的内容应该会给您带来编译器错误。
编辑:这个答案是错误的,尽管严格来说,代码是有效的。我只是把它留在这里,因为它下面的讨论太有用了。这个另一个答案是我上次编辑此内容时给出的最佳答案:How do I pass a unique_ptr argument to a constructor or a function?
其基本思想::std::move是,传递给您的人应该使用它来表达他们知道传递给您的人将失去所有权的unique_ptr知识。unique_ptr
这意味着您应该unique_ptr在方法中使用对 a 的右值引用,而不是 aunique_ptr本身。无论如何这都行不通,因为传递一个普通的旧值unique_ptr需要制作一个副本,而这在unique_ptr. 有趣的是,使用命名的右值引用会将其再次转换回左值,因此您也需要在方法::std::move 内部使用。
这意味着您的两个方法应该如下所示:
Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability
void setNext(Base::UPtr &&n) { next = ::std::move(n); }
Run Code Online (Sandbox Code Playgroud)
然后使用这些方法的人会这样做:
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
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,::std::move表示指针将在最相关且最有帮助的地方失去所有权。objptr如果这种情况在无形中发生,那么对于使用你的类的人来说,如果没有明显的原因突然失去了所有权,那将会非常令人困惑。
unique_ptr那样使用 's 。我相信你正在把事情搞得一团糟——对于那些需要阅读你的代码、维护它的人,甚至可能是那些需要使用它的人来说。
仅unique_ptr当您有公开公开的unique_ptr成员时才采用构造函数参数。
unique_ptr's 包装原始指针以进行所有权和生命周期管理。它们非常适合本地化使用,但对于接口来说并不好,实际上也不是有意的。想要界面吗?将您的新类记录为所有权,并让它获得原始资源;或者,对于指针,可以按照核心指南owner<T*>中的建议使用。
只有当您的类的目的是保存unique_ptr' 并让其他人unique_ptr这样使用它们时 - 只有这样您的构造函数或方法才合理地使用它们。
不要暴露您在内部使用 s 的事实unique_ptr。
用于unique_ptr列表节点很大程度上是一个实现细节。实际上,即使您让类似列表的机制的用户直接使用裸列表节点(自己构建它并将其提供给您),恕我直言,这也不是一个好主意。我不需要形成一个新的 list-node-which-is-also-a-list 来将某些内容添加到您的列表中 - 我应该只通过值、const lvalue ref 和/或 rvalue ref 传递有效负载。然后你就处理它。对于拼接列表 - 再次是 value、const lvalue 和/或 rvalue。
| 归档时间: |
|
| 查看次数: |
153444 次 |
| 最近记录: |